우리가 사용하는 브라우저는 HTTP 통신을 통해 클라이언트와 서버가 통신을 한다. 그런데 이 HTTP 규약은 stateless하다는 특징이 있다. 그럼 사용자가 로그인을 할 때 새 탭에서 로그인을 할 경우 매번 로그인을 시도해야 된다. 그래서 로그인을 할 때 정보를 어딘가에 저장해두면 문제를 쉽게 해결할 수가 있다.
그 때 사용하는 것이 바로 브라우저의 스토리지이다. 쿠키 같은 것을 사용해서 해당 로그인 정보를 브라우저에 저장해서 로그인을 편리하게 구현할 수 있다.
쿠키란?
- 쿠키는 서버가 클라이언트에게 전송하는 작은 데이터 파일이다.
- 쿠키는 클라이언트에 저장되고 key와 value로 구성된다.
- 서버에게 받은 쿠키는 정보들을 웹 브라우저에 저장하고
- 브라우저가 자동으로 요청 시마다 쿠키를 서버로 전송한다.
- 용량 제한이 있다. (도메인별 약 4KB 정도)
쿠키의 수명과 브라우저 동작
쿠키는 만료 시간(Expire/Max-Age)에 따라 세션 쿠키(브라우저 종료 시 삭제) / **영속 쿠키(만료일까지 유지)**로 나뉜다.
- 세션 쿠키 (Session Cookie): Expires나 Max-Age를 설정하지 않은 경우 기본값
- 브라우저 종료 시 자동 삭제
- 로그인 유지가 되지 않음
- 영속 쿠키 (Persistent Cookie): Expires(특정 날짜/시간)나 Max-Age(초 단위 유효시간)를 지정한 경우
- 지정한 시간이 될 때까지 브라우저를 껐다 켜도 그대로 남아있음
- 자동 로그인 유지 가능
{ Expires, Max-Age } 옵션을 지정하지 않으면 원래는 세션 쿠키가 되어야 한다. 즉, 브라우저를 닫으면 삭제되는 게 정상이다. 그런데 브라우저마다 동작이 좀 다르다!
- Chrome, Edge, 일부 최신 브라우저 → 세션 복원(Session Restore) 기능이 켜져 있으면, 세션 쿠키도 다시 복원해버린다. 그래서 브라우저를 종료했다 다시 열어도 쿠키가 남아있는 것처럼 보인다.
- Firefox, Safari → 옵션을 껐으면 세션 쿠키는 닫을 때 지워진다.
🔑 쿠키 로그인 방식
- 클라이언트가 로그인을 시도한다.
- 로그인을 성공했을 경우, 서버는 토큰을 생성해서 클라이언트에 전달한다.
- 클라이언트는 쿠키에 토큰을 저장한다.
- 해당 웹사이트를 방문할 때마다 쿠키는 서버에 보내지게 된다.
- 서버에서는 토큰이 유효한지 판단을 하며 클라이언트와 통신한다.
이 과정에서 중요한 부분은 클라이언트에 저장이 된다는 점이다. 이 사실은 여러 상황으로 직결될 수 있다.
- 모든 브라우저에서 지원한다.
- 서버에서 따로 저장을 하지 않기 때문에 서버 과부화가 일어나지 않는다.
- 누군가가 쿠키에 담긴 나의 토큰 정보를 빼앗긴다면 나인척 할 수 있다. (보안 취약)
보안적으로 봤을 때, 토큰이 탈취 될 가능성이 있어서 쿠키만 사용한다는 것은 좋은 생각이 아니다.
그래서 로그인을 유지시키기 위해서는 쿠키 방법에 세션 방법을 추가해서 중요한 정보는 서버에 저장하여 보안을 관리한다.
세션이란?
- 세션은 브라우저 저장소가 아니다.
- 세션은 서버 메모리/DB 같은 서버 측에 저장되는 정보이다.
- 세션은 “로그인한 사용자" 정보를 쿠키에 저장하지 않고 서버에 저장하며, 대신 쿠키에는 이를 식별할 수 있는
세션 ID를 저장한다. - 이처럼 세션도 쿠키를 이용하지만 추정 불가능한
세션 ID를 주고받기 때문에 보안상 안전하다. - 따라서 노출되면 안 되는 중요한 정보는 세션을 이용하여 저장한다.
- 세션은 서버에 저장되다 보니까, 너무 많이 담기면 서버 과부화가 일어날 가능성이 생긴다.
- 서버에서 사용되다 보니까, 쿠키보다는 탈취되는 과정이 어렵다.
⚠️ 주의) 여기서 말하는 세션은 세션 스토리지랑은 다른거다!
🔑 전통적인 세션 로그인 방식
- 클라이언트가 로그인을 시도한다.
- 로그인에 성공했을 경우, 서버는 클라이언트 고유 세션 ID를 생성하고 서버에 저장한다.
- 서버는 클라이언트에게 세션 ID를 전달하고 쿠키에 저장한다.
- 클라이언트는 세션 ID를 쿠키에서 가져오면서 서버와 통신한다.
- 서버는 세션 ID가 유효한지 확인한다.
세션 방식에서는 세션 ID의 반쪽은 사용자 브라우저에 쿠키로 저장되고, 나머지 반쪽은 서버에 저장된다고 생각하면 된다.
이처럼 이 세션 ID를 사용해서 어떤 사용자가 서버에 로그인 되었음이 지속되는 이 상태를 '세션'이라고 한다.
정리
- 쿠키 = 브라우저 저장소 (데이터 자체가 클라이언트에 있음)
- 세션 = 서버 저장소 (브라우저에는 "세션ID"만 저장, 본 데이터는 서버에 있음)
Express 환경에서 session + cookie 적용해보기
🔧 서버 개발 환경
Node.js: v22 (LTS)
Express: v5.1.0
템플릿 엔진 EJS: v3.1.10
low DB: NeDB
🤔 고민과 선택 과정
세션+쿠키 방식의 로그인을 적용하기 전에, express-session + nedb-session-store 모듈을 사용할지 직접 세션을 커스텀할지 고민하였다. 왜냐하면 세션ID를 만들어서 세션DB까지 구현하는 요구사항이라면, 굳이 모듈에 대한 의존성 없이도 커스텀 방식으로 구현은 가능하기 때문이다.
하지만 아래와 같은 이유로 각각의 특징과 트레이드오프를 생각해서 모듈 방식으로 선택했다.
커스텀 세션 방식 (NeDB+sid)
- 현재 NeDB를 사용중이라 동일한 스토리지로 간단하게 확장이 가능하다. 따로 세션 저장소 구현체를 설치하지 않아도 된다.
- 커스텀 로직(세션 스키마, 만료 정책 등)을 세밀하게 제어하기에 용이하다.
- 의존성 최소화하기 위해 굳이 모듈이 없어도 요구사항을 충족할 수 있다.
- 쿠키와 세션 옵션을 따로 설정해줘야 한다.
// 세션 생성 및 쿠키 설정 const sessionId = randomUUID(); const now = Date.now(); const maxAgeMs = 1000 * 60 * 60 * 24 * 7; // 7일 await createSession({ sessionId, username: user.username, nickname: user.nickname, createdAt: new Date(now).toISOString(), expiresAt: new Date(now + maxAgeMs), }); res.cookie("sid", sessionId, { httpOnly: true, sameSite: "lax", maxAge: maxAgeMs, });
모듈 방식 (express-session, nedb-session-store)
- express-session를 사용하면 세션 ID를 저장하기 위한 호환되는 세션 스토어 사용을 권장한다.
- 이후 Redis나 MongoDB와 같은 스토어와 교체 용이성이 좋다.
- 세션 스토어를 사용하면 Passport, Flash, CSRF 등 express-session에 기대는 미들웨어를 함께 쓸 때 유용하다.
- 쿠키와 세션 옵션이 일원화되어 있어 편리하게 로직을 관리할 수 있다.
app.use( session({ secret: "your-secret", resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: "lax", }, }), );
요구사항 충족과 더불어 확장성을 고려해서 모듈 방식을 선택했다. lowDB로 NeDB를 선택했던 것도 mongoDB와 유사성 및 교체 용이성을 바라보고 결정했기 때문에 이번에도 스토어와의 교체 용이성을 위해 nedb-session-store를 사용했다. 그리고 모듈의 쿠키+세션 옵션이 간편한 것도 좋았다.
구현 과정
- session과 cookie-parser, nedb-session-store 모듈 설치
npm i express-session cookie-parser nedb-session-store
express-session: 세션 관리용 미들웨어로 세션 관리 시 클라이언트에 세션 쿠키를 보낸다.cookie-parser: 요청과 함께 들어온 쿠키를 해석하여 곧바로 req.cookies객체로 만든다.nedb-session-store: 세션 데이터를 NeDB 파일(DB)로 저장하게 해주는 어댑터로 express-session의 세션 저장소(store) 구현체 중 하나이다.
- app.js 설정
- 세션은 사용자별로 req.session 객체 안에 유지된다.
- 안전하게 쿠키를 전송하려면 쿠키에 서명을 추가해야하고, 쿠키를 서명할 때 secret 값이 필요하다.
cookie-parser의 secret과 같게 설정하는 것이 좋다.
const cookieParser = require("cookie-parser");
const session = require("express-session");
const NedbStore = require("nedb-session-store")(session);
// cookieParser에 비밀키를 넣어 요청온 쿠키값이 내가 서명한 쿠키인지 파악한다.
// 암호 키를 작성하는 것에는 크게 규격이 없으며 개발자의 자유이다. 단 쉽게 유추할 수 있는 값은 사용하지 말자.
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
session({
secret: process.env.COOKIE_SECRET, // 암호화하는 데 쓰일 키
resave: false, // 세션을 언제나 저장할지 설정함
saveUninitialized: true, // 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정
store: new NedbStore({ filename: "./data/sessions.db" }), // 세션 객체에 세션스토어를 적용
cookie: {
//세션 쿠키 설정 (세션 관리 시 클라이언트에 보내는 쿠키)
maxAge: 1000 * 60 * 60 * 24 * 7,
httpOnly: true, // 자바스크립트를 통해 세션 쿠키를 사용할 수 없도록 함
sameSite: "lax", // CSRF 위험을 줄임, 완전 차단은 아니고 “안전한” 탐색에서만 허용
// Secure: true // https 환경에서만 session 정보를 주고받도록처리
},
// name: 'session-cookie' // 세션 쿠키명 디폴트값은 connect.sid이지만 다른 이름을 줄수도 있다.
}),
);
app.get("/", (req, res, next) => {
// 세션에 데이터를 설정하면, 모든 세션이 설정되는게아니라, 요청 받은 고유의 세션 사용자의 값만 설정 된다.
// 즉, 개인의 저장 공간이 생긴 것과 같다.
req.session.id = "hello";
});
✍️ 인증(서명)된 쿠키
cookieParser(process.env.COOKIE_SECRET)
cookieParser의 첫번째 인수로 비밀 키를 넣어줄 수 있다.
서명된 쿠키가 있는 경우, 제공한 비밀 키를 통해 해당 쿠키가 내가 만든 쿠키임을 검증할 수 있다.
쿠키는 클라이언트에서 위조하기 쉬우므로, 비밀 키를 통해 만들어낸 서명을 쿠키 값 뒤에 붙이는 것이다!
이렇게 작성하면 서명된 쿠키를 생성하고 활용할 때 서버와 클라이언트 PC만 알아볼 수 있도록 통신하게 된다.
💾 세션 스토어
세션 스토어는 세션이 데이터를 저장하는 곳을 말한다.
대표적으로 Memory Store, File Store, Mongo Store 가 있다.
default 값은 Memory Store인데, 메모리는 서버나 클라이언트를 껐다 키면 사라지는 휘발성이다.
그래서 세션을 저장할 고유 저장소를 따로 지정해야 한다.
- 어떤 DB를 사용하는지 따라서 store를 이 API 문서를 보고 선택하면 된다.
필자는 인메모리 방식인 nedb-session-store를 설치해서 lowDB로 저장 관리했다.
이렇게 하고 나서 처음 서버를 올리면 sessions.db 가 생긴다. 사용자가 로그인에 성공해서 서버에 접속할 때 이 sessions.db에 유저 정보의 세션 ID가 생긴다.
{
"_id": "RIERELq-eIFQ-Uw9ubtI1mL749qAhu_K",
"session": {
"cookie": {
"originalMaxAge": 604800000,
"expires": {
"$$date": 1756807508766
},
"httpOnly": true,
"path": "/",
"sameSite": "lax"
},
"user": {
"username": "ekdus",
"nickname": "dayeonkim"
}
},
"expiresAt": {
"$$date": 1756807578016
},
"createdAt": {
"$$date": 1756202708767
},
"updatedAt": {
"$$date": 1756202778017
}
}
_id 가 세션 ID로 생성되었다.
브라우저에서 세션 쿠키를 확인해보면
connect.sid: s:RIERELq-eIFQ-Uw9ubtI1mL749qAhu_K.C5F3R%2FIMTdZkfQVa%2FiBld1Z6zn0CwyQ3ifjwjDVejQo
connect.sid로 같은 RIERELq-eIFQ-Uw9ubtI1mL749qAhu_K 값으로 생성된걸 확인할 수 있고, .뒤에 C5F3R%2FIMTdZkfQVa%2FiBld1Z6zn0CwyQ3ifjwjDVejQo 값은 COOKIE_SECRET으로 서명(signature)된 값이다. 이는 암호화(encryption)와는 다른 값이다. s는 서명(signed) 쿠키라는 표시이다.
- loginController 설정
// POST /login
const postLogin = async (req, res) => {
try {
const { username, password } = req.body;
const user = await findUserByUsername(username);
// 세션에 사용자 정보 저장
req.session.user = { username: user.username, nickname: user.nickname };
return res.status(200).json({ success: true, message: "로그인 성공" });
} catch (err) {
console.error("postLogin error:", err);
return res.status(500).json({ success: false, message: "서버 오류" });
}
};
req.session.user에 사용자 정보(이름, 닉네임)를 저장해서 세션 스토어에 값이 같이 저장되도록 했다. 이렇게 하면 로그인 이후 요청들에서 미들웨어가 connect.sid로 세션을 찾아 req.session.user를 다시 채워준다. 그래서 매번 DB 조회할 필요없이 어디서든 req.session.user로 로그인 상태/사용자 식별이 가능하다.
세션의 한계와 보안 이슈
세션은 쿠키보다 안전해 보이지만, 단점도 존재한다.
-
서버 자원 부담
세션은 서버 메모리/DB에 저장되므로, 동시 접속자가 많을수록 서버에 부하가 생길 수 있다.
(해결책: Redis 같은 외부 세션 저장소를 사용) -
확장성 문제
서버를 여러 대 운영할 때 세션을 공유하지 않으면 사용자마다 로그인 상태가 달라지는 문제가 생긴다.
(해결책: 공용 세션 저장소 사용) -
세션 하이재킹 위험
세션 ID가 탈취되면, 아이디/비밀번호를 몰라도 그대로 로그인 상태가 된다.
(예: XSS, 네트워크 스니핑, 세션 고정 공격 등)
방어 방법
- 쿠키 보안 속성
- HttpOnly: true → JS에서 접근 차단(XSS 방어)
- Secure: true → HTTPS에서만 전송
- SameSite: strict/lax → CSRF 공격 방어
- 세션 관리
- 로그인 성공 시 세션 ID를 새로 발급(Session Regeneration)
- 세션에 IP, User-Agent 등을 기록해서 매 요청마다 검증
- 일정 시간 활동 없으면 세션 만료 (Idle Timeout)
- 전송 보안
- HTTPS 필수 → 네트워크에서 세션 ID 탈취 방지 확장성
- Redis 같은 외부 세션 스토어 사용 → 서버 여러 대 운영 시에도 안정적
이런 방법들을 조합해야 안전한 세션 관리가 가능하다.
참고자료
cookie-parser, sign
- https://expressjs.com/en/resources/middleware/cookie-parser.html
- https://expressjs.com/en/api.html#res.cookie
- https://inpa.tistory.com/entry/EXPRESS-%F0%9F%93%9A-bodyParser-cookieParser-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4#express_-_cookie-parser
session 미들웨어
connect.sid:
s:RIERELq-eIFQ-Uw9ubtI1mL749qAhu_K.C5F3R%2FIMTdZkfQVa%2FiBld1Z6zn0CwyQ3ifjwjDVejQo