1. 쿠키의 개념
쿠키는 웹 서버가 사용자의 브라우저에 저장하는 작은 텍스트 데이터입니다. HTTP는 무상태(stateless) 프로토콜이므로, 쿠키를 통해 사용자를 식별하고 상태를 유지합니다. 쿠키는 로그인 상태 유지, 사용자 설정 저장, 방문 기록 추적 등에 활용됩니다.
1.1 HTTP 쿠키 동작 원리
1. 클라이언트 → 서버: 최초 요청
2. 서버 → 클라이언트: 응답 헤더에 Set-Cookie 포함
Set-Cookie: sessionId=abc123; HttpOnly; Secure
3. 클라이언트: 쿠키를 브라우저에 저장
4. 클라이언트 → 서버: 이후 요청마다 Cookie 헤더에 자동 포함
Cookie: sessionId=abc123
서버는 응답 시 Set-Cookie 헤더로 쿠키를 설정하고, 브라우저는 이후 같은 도메인으로 요청할 때 Cookie 헤더에 저장된 쿠키를 자동으로 포함합니다.
2. cookie-parser 미들웨어 사용
Express.js에서 쿠키를 다루려면 cookie-parser 미들웨어를 사용합니다.
npm install cookie-parser
2.1 기본 설정
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
// cookie-parser 미들웨어 등록
app.use(cookieParser());
app.get('/', (req, res) => {
// req.cookies로 쿠키 접근
console.log(req.cookies);
res.send('쿠키 확인');
});
app.listen(3000);
cookie-parser를 등록하면 req.cookies 객체를 통해 클라이언트가 보낸 쿠키에 접근할 수 있습니다.
2.2 서명된 쿠키를 위한 설정
// 비밀 키를 전달하면 서명된 쿠키 사용 가능
app.use(cookieParser('my-secret-key'));
// 서명된 쿠키는 req.signedCookies로 접근
app.get('/', (req, res) => {
console.log(req.cookies); // 일반 쿠키
console.log(req.signedCookies); // 서명된 쿠키
res.send('확인');
});
3. 쿠키 설정 옵션
res.cookie() 메서드로 쿠키를 설정할 때 다양한 옵션을 지정할 수 있습니다.
res.cookie('name', 'value', {
httpOnly: true, // JavaScript에서 접근 불가 (XSS 방지)
secure: true, // HTTPS에서만 쿠키 전송
maxAge: 1000 * 60 * 60, // 밀리초 단위 (1시간)
expires: new Date(), // 만료 날짜 (Date 객체)
domain: 'example.com', // 쿠키가 유효한 도메인
path: '/', // 쿠키가 유효한 경로
sameSite: 'strict', // CSRF 방지 설정
signed: true // 서명된 쿠키 사용
});
3.1 옵션 상세 설명
| 옵션 | 설명 | 권장값 |
|---|---|---|
| httpOnly | true면 클라이언트 JavaScript에서 접근 불가 | true |
| secure | true면 HTTPS에서만 쿠키 전송 | 프로덕션: true |
| maxAge | 쿠키 유효 기간 (밀리초) | 용도에 따라 설정 |
| expires | 쿠키 만료 시점 (Date 객체) | maxAge 권장 |
| domain | 쿠키가 전송되는 도메인 | 기본값 사용 |
| path | 쿠키가 전송되는 경로 | '/' |
| sameSite | CSRF 방지 (strict, lax, none) | strict 또는 lax |
| signed | 서명 여부 | 민감 데이터: true |
3.2 sameSite 옵션
// strict: 같은 사이트 요청에서만 쿠키 전송
res.cookie('session', 'abc', { sameSite: 'strict' });
// lax: GET 요청으로 외부 링크 클릭 시에도 쿠키 전송 (기본값)
res.cookie('session', 'abc', { sameSite: 'lax' });
// none: 크로스 사이트 요청에도 쿠키 전송 (secure: true 필수)
res.cookie('session', 'abc', { sameSite: 'none', secure: true });
4. 서명된 쿠키(Signed Cookie)
서명된 쿠키는 쿠키 값의 무결성을 검증할 수 있습니다. 클라이언트가 쿠키 값을 임의로 변경했는지 확인할 수 있습니다.
4.1 서명된 쿠키 설정 및 읽기
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
// 비밀 키로 cookie-parser 초기화
app.use(cookieParser('my-super-secret-key'));
// 서명된 쿠키 설정
app.get('/set-signed', (req, res) => {
res.cookie('userId', '12345', {
signed: true,
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 // 1일
});
res.json({ message: '서명된 쿠키 설정 완료' });
});
// 서명된 쿠키 읽기
app.get('/get-signed', (req, res) => {
const userId = req.signedCookies.userId;
if (userId) {
res.json({ userId });
} else {
// 서명 검증 실패 또는 쿠키 없음
res.status(400).json({ error: '유효하지 않은 쿠키' });
}
});
app.listen(3000);
서명된 쿠키는 값이 변조되면 req.signedCookies에서 해당 키가 false로 반환됩니다. 쿠키가 존재하지 않으면 undefined입니다.
4.2 서명 원리
설정 시:
value = '12345'
signature = HMAC-SHA256('12345', 'my-super-secret-key')
실제 쿠키 값 = 's:12345.signature'
읽기 시:
1. 쿠키 값에서 원본 데이터와 서명 분리
2. 원본 데이터로 서명 재계산
3. 전달받은 서명과 비교
4. 일치하면 원본 값 반환, 불일치하면 false
5. 쿠키 CRUD 실제 예제
5.1 쿠키 설정 (Create)
app.get('/set-cookie', (req, res) => {
// 기본 쿠키
res.cookie('username', 'hong');
// 옵션 포함 쿠키
res.cookie('preferences', JSON.stringify({ theme: 'dark', lang: 'ko' }), {
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 * 30 // 30일
});
// 서명된 쿠키
res.cookie('token', 'abc123xyz', {
signed: true,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
});
res.json({ message: '쿠키 설정 완료' });
});
5.2 쿠키 읽기 (Read)
app.get('/get-cookie', (req, res) => {
// 일반 쿠키 읽기
const username = req.cookies.username;
// JSON 쿠키 파싱
let preferences = null;
if (req.cookies.preferences) {
try {
preferences = JSON.parse(req.cookies.preferences);
} catch (e) {
preferences = null;
}
}
// 서명된 쿠키 읽기
const token = req.signedCookies.token;
res.json({
username,
preferences,
token: token || null,
allCookies: req.cookies,
allSignedCookies: req.signedCookies
});
});
5.3 쿠키 수정 (Update)
app.get('/update-cookie', (req, res) => {
// 같은 이름으로 다시 설정하면 덮어쓰기
res.cookie('username', 'kim', {
httpOnly: true,
maxAge: 1000 * 60 * 60
});
res.json({ message: '쿠키 수정 완료' });
});
5.4 쿠키 삭제 (Delete)
app.get('/delete-cookie', (req, res) => {
// clearCookie 사용
res.clearCookie('username');
// 옵션이 있는 쿠키는 동일한 옵션으로 삭제해야 함
res.clearCookie('preferences', {
httpOnly: true,
path: '/'
});
// 서명된 쿠키 삭제
res.clearCookie('token', {
signed: true,
httpOnly: true,
path: '/'
});
res.json({ message: '쿠키 삭제 완료' });
});
쿠키 삭제 시 설정할 때 사용한 path, domain 옵션을 동일하게 지정해야 삭제됩니다.
5.5 종합 예제
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cookieParser('secret-key-123'));
app.use(express.json());
// 방문 횟수 카운터
app.get('/visit', (req, res) => {
let visits = parseInt(req.cookies.visits) || 0;
visits++;
res.cookie('visits', visits.toString(), {
maxAge: 1000 * 60 * 60 * 24 * 365, // 1년
httpOnly: true
});
res.json({ message: `${visits}번째 방문입니다.` });
});
// 사용자 설정 저장
app.post('/settings', (req, res) => {
const { theme, fontSize, language } = req.body;
const settings = { theme, fontSize, language };
res.cookie('userSettings', JSON.stringify(settings), {
signed: true,
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 * 30 // 30일
});
res.json({ message: '설정 저장 완료', settings });
});
// 사용자 설정 불러오기
app.get('/settings', (req, res) => {
const settingsCookie = req.signedCookies.userSettings;
if (!settingsCookie) {
return res.json({ settings: null });
}
try {
const settings = JSON.parse(settingsCookie);
res.json({ settings });
} catch (e) {
res.json({ settings: null });
}
});
// 로그아웃 (관련 쿠키 모두 삭제)
app.post('/logout', (req, res) => {
res.clearCookie('visits');
res.clearCookie('userSettings', { signed: true });
res.json({ message: '로그아웃 완료' });
});
app.listen(3000);
6. 보안 고려사항
6.1 필수 보안 설정
// 프로덕션 환경 쿠키 설정
const cookieOptions = {
httpOnly: true, // XSS 공격 방지
secure: process.env.NODE_ENV === 'production', // HTTPS 전용
sameSite: 'strict', // CSRF 공격 방지
maxAge: 1000 * 60 * 15, // 15분 (짧게 유지)
path: '/',
signed: true // 변조 방지
};
// 민감 정보 쿠키
res.cookie('sessionToken', token, cookieOptions);
6.2 주요 보안 위협과 대응
| 위협 | 설명 | 대응 방법 |
|---|---|---|
| XSS | 악성 스크립트로 쿠키 탈취 | httpOnly: true |
| CSRF | 사용자 권한으로 악의적 요청 | sameSite: strict |
| 중간자 공격 | 네트워크에서 쿠키 가로채기 | secure: true |
| 쿠키 변조 | 클라이언트가 값 임의 수정 | signed: true |
6.3 보안 체크리스트
const express = require('express');
const cookieParser = require('cookie-parser');
const helmet = require('helmet');
const app = express();
// 보안 헤더 설정
app.use(helmet());
// 프록시 환경 (로드밸런서 뒤)에서 secure 쿠키 사용
app.set('trust proxy', 1);
// 강력한 비밀 키 사용
const COOKIE_SECRET = process.env.COOKIE_SECRET;
if (!COOKIE_SECRET || COOKIE_SECRET.length < 32) {
throw new Error('COOKIE_SECRET must be at least 32 characters');
}
app.use(cookieParser(COOKIE_SECRET));
// 보안 쿠키 설정 함수
function setSecureCookie(res, name, value, maxAge = 3600000) {
res.cookie(name, value, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
signed: true,
maxAge,
path: '/'
});
}
6.4 민감 정보 저장 금지
// 잘못된 예: 민감 정보를 쿠키에 저장
res.cookie('password', userPassword); // 절대 금지
res.cookie('creditCard', cardNumber); // 절대 금지
// 올바른 예: 식별자만 저장하고 서버에서 조회
res.cookie('sessionId', 'random-session-id', {
httpOnly: true,
secure: true,
signed: true
});
7. 실무 활용 팁
7.1 환경별 쿠키 설정
const cookieConfig = {
development: {
httpOnly: true,
secure: false,
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24 // 1일
},
production: {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 1000 * 60 * 60 // 1시간
}
};
const env = process.env.NODE_ENV || 'development';
const currentConfig = cookieConfig[env];
app.get('/login', (req, res) => {
res.cookie('session', sessionId, currentConfig);
});
7.2 쿠키 유틸리티 함수
// 쿠키 관리 유틸리티
const CookieManager = {
set(res, name, value, options = {}) {
const defaultOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
signed: true,
path: '/'
};
res.cookie(name, value, { ...defaultOptions, ...options });
},
get(req, name, signed = true) {
return signed ? req.signedCookies[name] : req.cookies[name];
},
delete(res, name, options = {}) {
const defaultOptions = {
path: '/',
signed: true
};
res.clearCookie(name, { ...defaultOptions, ...options });
},
setJSON(res, name, data, options = {}) {
this.set(res, name, JSON.stringify(data), options);
},
getJSON(req, name, signed = true) {
const value = this.get(req, name, signed);
if (!value) return null;
try {
return JSON.parse(value);
} catch (e) {
return null;
}
}
};
// 사용 예시
app.get('/example', (req, res) => {
CookieManager.setJSON(res, 'cart', { items: ['item1', 'item2'] });
const cart = CookieManager.getJSON(req, 'cart');
res.json({ cart });
});
7.3 쿠키 크기 제한
// 쿠키 최대 크기는 약 4KB
// 큰 데이터는 서버에 저장하고 ID만 쿠키에 저장
app.post('/save-data', (req, res) => {
const data = req.body;
const dataSize = JSON.stringify(data).length;
if (dataSize > 3000) { // 안전 마진 확보
// 서버/DB에 저장하고 ID만 쿠키에
const dataId = saveToDatabase(data);
res.cookie('dataRef', dataId, { signed: true, httpOnly: true });
} else {
// 작은 데이터는 쿠키에 직접 저장
res.cookie('data', JSON.stringify(data), { signed: true, httpOnly: true });
}
res.json({ message: '저장 완료' });
});
7.4 쿠키 동의 처리
// GDPR 등 규정 준수를 위한 쿠키 동의 확인
app.use((req, res, next) => {
const hasConsent = req.cookies.cookieConsent === 'true';
req.cookieConsent = hasConsent;
next();
});
app.post('/cookie-consent', (req, res) => {
const { accepted } = req.body;
if (accepted) {
res.cookie('cookieConsent', 'true', {
maxAge: 1000 * 60 * 60 * 24 * 365, // 1년
httpOnly: false // 클라이언트에서 확인 필요
});
}
res.json({ message: '동의 처리 완료' });
});
// 동의 여부에 따른 처리
app.get('/track', (req, res) => {
if (!req.cookieConsent) {
return res.json({ tracked: false, reason: 'no consent' });
}
// 추적 로직 실행
res.json({ tracked: true });
});
결론
쿠키는 HTTP의 무상태 특성을 보완하여 사용자 상태를 유지하는 핵심 메커니즘입니다. Express.js에서 cookie-parser 미들웨어를 사용하면 쿠키를 쉽게 설정하고 읽을 수 있습니다. 보안을 위해 httpOnly, secure, sameSite 옵션을 적절히 설정하고, 민감한 데이터는 서명된 쿠키를 사용하거나 서버에 저장하는 것이 좋습니다. 실무에서는 환경별 설정 분리, 유틸리티 함수 활용, 쿠키 크기 제한 준수를 통해 안전하고 효율적인 쿠키 관리가 가능합니다.
'Node.js' 카테고리의 다른 글
| Node.js의 세션 관리(Session Management) (0) | 2026.03.15 |
|---|---|
| Node.js의 OAuth2.0 구현 (1) | 2026.03.15 |
| Node.js의 JSON 웹 토큰(JWT) 사용법 (0) | 2026.03.14 |
| Node.js의 Socket.IO 사용법 (0) | 2026.03.14 |
| Node.js의 인증 및 인가(Authentication and Authorization) (0) | 2026.03.13 |