웹 개발을 하면서 사용자 로그인을 처리해야 할 때 쿠키, 세션, 토큰이라는 세 가지 방식을 마주치게 된다. 다만 이 세 가지가 정확히 뭐가 다른지, 어떤 상황에서 뭘 써야 하는지 설명할 수 있는 개발자는 생각보다 적다. 많은 사람들이 그냥 "세션을 쓰면 된다더라" 또는 "요즘은 토큰을 쓴다더라" 정도만 알고 있을 뿐이다. 

 

다만 각 방식의 원리를 이해하면, 프로젝트 특성에 맞는 최적의 인증 방식을 선택할 수 있다. 특히 모바일 앱, API 서버, 마이크로서비스 같은 최신 아키텍처에서는 올바른 선택이 매우 중요하다. 이번에는 쿠키, 세션, 토큰이 정확히 뭔지, 어떻게 작동하는지, 그리고 어떤 상황에서 뭘 써야 하는지 완벽하게 정리해서 소개하겠다. 

 

HTTP의 근본적인 문제: Stateless

먼저 쿠키, 세션, 토큰이 왜 필요한지 이해해야 한다.

HTTP는 기본적으로 "stateless"(상태를 유지하지 않음) 프로토콜이다. 즉, 요청(request)을 보낼 때마다 서버는 "이 사람이 누구인지" 알 수 없다.

사용자: "안녕, 나 홍길동이야"
서버: "반가워, 뭐 도와줄까?"

사용자: "내 정보 좀 봐"
서버: "어? 누구신데요?"  ← 이전 대화를 기억하지 못함

이 문제를 해결하기 위해 세 가지 방식이 나왔다: 쿠키, 세션, 토큰. 

 

1. 쿠키 (Cookie)

정의: 클라이언트(브라우저)에 저장되는 작은 데이터

쿠키는 서버가 사용자 정보를 담아 브라우저로 보내고, 브라우저는 그것을 저장했다가 매 요청마다 자동으로 함께 보낸다.

1. 사용자가 로그인
   사용자 → 서버: "홍길동, 비번 1234"
   서버: 검증 완료 → 쿠키 생성
   서버 → 사용자: "로그인 성공! 쿠키: user_id=123" 
   (브라우저가 자동 저장)

2. 사용자가 다른 페이지 접속
   사용자 → 서버: 자동으로 쿠키 포함해서 요청 (user_id=123)
   서버: "아, 123번 사용자네!" 인증 완료

 

쿠키의 특징:

  • 저장 위치: 클라이언트 (브라우저)
  • 크기: 작음 (보통 4KB 이하)
  • 유효기간: 설정 가능 (일정 시간 후 자동 삭제)
  • 보안: 브라우저에 평문으로 저장되어 비교적 취약
  • 전송 방식: 모든 HTTP 요청에 자동 포함

 

쿠키 설정 예제 (PHP):

<?php
// 로그인 성공 후 쿠키 설정
setcookie('user_id', '123', time() + (86400 * 30)); // 30일 유효
setcookie('username', '홍길동', time() + (86400 * 30));

// 쿠키 읽기
echo $_COOKIE['user_id'];   // 123
echo $_COOKIE['username'];  // 홍길동

// 쿠키 삭제 (유효기간을 과거로 설정)
setcookie('user_id', '', time() - 3600);
?>

 

쿠키 설정 예제 (JavaScript):

// 쿠키 설정
document.cookie = 'user_id=123; max-age=2592000; path=/';  // 30일
document.cookie = 'username=홍길동; max-age=2592000; path=/';

// 쿠키 읽기
var cookies = document.cookie;
console.log(cookies);  // "user_id=123; username=홍길동"

// 쿠키 삭제
document.cookie = 'user_id=; max-age=0; path=/';

 

쿠키의 문제점:

  • XSS(Cross-Site Scripting) 공격: JavaScript로 쿠키를 탈취할 수 있음
  • 모든 요청에 포함: 불필요한 데이터 전송으로 성능 저하
  • 크기 제한: 저장할 데이터가 크면 문제
  • 크로스 도메인: 다른 도메인간 공유 불가능

 

2. 세션 (Session)

정의: 서버에 저장되는 사용자 정보

세션은 쿠키와 반대 개념이다. 사용자 데이터는 서버에 저장하고, 브라우저는 "세션 ID"라는 식별자만 쿠키로 받는다.

1. 사용자가 로그인
   사용자 → 서버: "홍길동, 비번 1234"
   서버: 검증 완료 → 세션 생성
   서버의 메모리/파일: session_abc123 = {user_id: 123, name: '홍길동'}
   서버 → 사용자: 쿠키 설정 (PHPSESSID=abc123)

2. 사용자가 다른 페이지 접속
   사용자 → 서버: 자동으로 쿠키 포함 (PHPSESSID=abc123)
   서버: 세션 테이블에서 abc123 조회 → 사용자 정보 확인

 

세션의 특징:

  • 저장 위치: 서버 (메모리, 파일, 데이터베이스)
  • 크기: 크기 제한 없음
  • 보안: 서버에 저장되어 비교적 안전 (세션 ID만 노출)
  • 유효기간: 서버에서 관리
  • 서버 자원: 세션을 저장하기 위해 서버 메모리 필요

 

세션 사용 예제 (PHP):

<?php
// 세션 시작 (페이지 가장 위에 작성)
session_start();

// 로그인 성공 후 세션에 저장
$_SESSION['user_id'] = 123;
$_SESSION['username'] = '홍길동';
$_SESSION['login_time'] = time();

// 세션 읽기
echo $_SESSION['user_id'];     // 123
echo $_SESSION['username'];    // 홍길동

// 특정 세션 삭제
unset($_SESSION['user_id']);

// 세션 전체 삭제
session_destroy();
?>

 

세션 저장 방식:

// php.ini에서 설정
session.save_handler = files  // 파일로 저장 (기본값)
// 또는
session.save_handler = redis  // Redis에 저장 (성능 좋음)
session.save_handler = memcached  // Memcached에 저장 (확장성 좋음)

 

세션의 문제점:

  • 서버 메모리 사용: 사용자가 많아질수록 서버 부담 증가
  • 확장성 문제: 여러 서버가 있을 때 세션 공유 복잡
  • 모바일/API 부적합: 쿠키 기반이라 모바일 앱에서 사용 어려움
  • CSRF 공격 가능: 쿠키 기반이라 악의적 요청이 자동으로 쿠키 포함

 

3. 토큰 (Token)

정의: 서명된 정보를 담은 문자열

토큰은 JWT(JSON Web Token) 같은 형태로, 사용자 정보를 암호화해서 클라이언트에 줄 뿐 서버는 보관하지 않는다.

1. 사용자가 로그인
   사용자 → 서버: "홍길동, 비번 1234"
   서버: 검증 완료 → 토큰 생성 및 서명
   token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsIm5hbWUiOiLhhYDhhKDhhZXhhYEifQ...
   서버 → 사용자: 토큰 반환 (저장하라고 알려줌)

2. 사용자가 API 요청
   사용자 → 서버: HTTP Header에 토큰 포함
   Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
   서버: 토큰 검증 (서명 확인) → 유효하면 요청 처리

 

토큰의 특징:

  • 저장 위치: 클라이언트 (localStorage, 메모리, 쿠키 등)
  • 서버 저장: 서버는 토큰을 저장하지 않음 (stateless)
  • 보안: 토큰에 서명이 포함되어 위조 불가능
  • 확장성: 여러 서버에서 같은 토큰 검증 가능
  • 적용 범위: 웹, 모바일 앱, API 등 어디든 사용 가능

 

JWT 토큰 구조:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsIm5hbWUiOiLhhYDhhKDhhZXhhYEifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  ↑                                  ↑                                                           ↑
 Header (알고리즘)                 Payload (데이터)                                      Signature (서명)

Header: {"alg":"HS256","typ":"JWT"}
Payload: {"user_id":123,"name":"홍길동","exp":1642424400}
Signature: HMACSHA256(base64(header)+"."+base64(payload), secret_key)

 

토큰 생성 예제 (PHP):

<?php
// JWT 라이브러리 필요 (composer require firebase/jwt)
require 'vendor/autoload.php';
use FirebaseJWTJWT;

// 로그인 성공 후 토큰 생성
$key = 'your_secret_key';
$payload = array(
   'user_id' => 123,
   'username' => '홍길동',
   'email' => 'hong@example.com',
   'exp' => time() + 3600  // 1시간 유효
);

$token = JWT::encode($payload, $key, 'HS256');
echo json_encode(['token' => $token]);
?>

 

토큰 검증 예제 (PHP):

<?php
require 'vendor/autoload.php';
use FirebaseJWTJWT;
use FirebaseJWTKey;

// HTTP Header에서 토큰 추출
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? '';
$token = str_replace('Bearer ', '', $authHeader);

try {
   $key = 'your_secret_key';
   $decoded = JWT::decode($token, new Key($key, 'HS256'));
   
   echo '토큰 유효함';
   echo $decoded->user_id;      // 123
   echo $decoded->username;     // 홍길동
   
} catch(Exception $e) {
   echo '토큰 유효하지 않음: ' . $e->getMessage();
}
?>

 

토큰 사용 예제 (JavaScript/AJAX):

// 1. 로그인 후 토큰 받기
$.post('/login', {username: 'hong', password: '1234'}, function(response) {
   var token = response.token;
   localStorage.setItem('auth_token', token);  // 로컬 스토리지에 저장
});

// 2. API 요청 시 토큰 포함
var token = localStorage.getItem('auth_token');
$.ajax({
   url: '/api/user/profile',
   type: 'GET',
   headers: {
      'Authorization': 'Bearer ' + token
   },
   success: function(data) {
      console.log(data);
   }
});

// 3. 로그아웃 (토큰 삭제)
localStorage.removeItem('auth_token');

 

토큰의 문제점:

  • 토큰 취소 어려움: 서버에 저장되지 않아 로그아웃 후에도 유효
  • 토큰 탈취: localStorage에 저장되면 XSS 공격에 취약
  • Refresh Token 필요: 보안상 만료 시간이 짧아 Refresh Token 필요
  • 크기: 토큰에 정보가 많으면 크기가 커짐

 

세 가지 비교표
특징 쿠키 세션 토큰
저장 위치 클라이언트 서버 클라이언트
서버 부담 적음 많음 적음
보안 낮음 높음 높음
확장성 나쁨 나쁨 우수
모바일 앱 불편 불편 적합
API 서버 불적합 불적합 매우 적합
로그아웃 쉬움 쉬움 어려움
CSRF 공격 취약 취약 안전

 

어떤 상황에서 뭘 써야 할까?

쿠키 사용:

  • 간단한 웹사이트 (블로그, 정보 사이트)
  • 사용자 선호도 저장 (언어, 테마 등)
  • 실시간 성능이 중요하지 않은 경우
  • 요즘은 거의 사용하지 않음

 

세션 사용:

  • 전통적인 웹 애플리케이션 (ASP.NET, PHP, JSP)
  • 단일 서버 환경
  • 보안이 중요한 금융/관리 사이트
  • 사용자 수가 많지 않은 서비스

 

토큰(JWT) 사용:

  • REST API 서버
  • 모바일 앱
  • 마이크로서비스 아키텍처
  • 다중 서버/클라우드 환경
  • 요즘 대부분의 신규 프로젝트

 

보안 팁

쿠키 보안:

// HttpOnly: JavaScript에서 접근 불가 (XSS 공격 방지)
setcookie('user_id', '123', time() + 3600, '/', '', false, true);
//                                                         ↑
//                                                    HttpOnly = true

// Secure: HTTPS에서만 전송
setcookie('user_id', '123', time() + 3600, '/', '', true, true);
//                                                 ↑
//                                            Secure = true

// SameSite: CSRF 공격 방지
header('Set-Cookie: user_id=123; SameSite=Strict');

 

토큰 보안:

  • Refresh Token 사용 (Access Token은 1시간, Refresh Token은 7일)
  • 토큰을 쿠키의 HttpOnly에 저장 (localStorage 대신)
  • 토큰에 민감한 정보 포함 금지
  • HTTPS 필수
  • 서버에서 토큰 만료 시간 검증

 

세션 보안:

  • 세션 ID를 안전한 난수로 생성
  • 세션 타임아웃 설정
  • https 필수
  • 세션 파일 권한 제한 (644)

 

현실적인 해결책

가장 안전한 방식은 토큰 + Refresh Token 조합이다:

// 1. 로그인 시 두 가지 토큰 발급
{
   "access_token": "eyJhbGc...",    // 1시간 유효
   "refresh_token": "eyJhbGc...",   // 7일 유효
   "expires_in": 3600
}

// 2. API 요청: Access Token 사용
$.ajax({
   headers: {
      'Authorization': 'Bearer ' + access_token
   }
});

// 3. Access Token 만료 시: Refresh Token으로 새로운 Access Token 발급
$.post('/token/refresh', {
   refresh_token: refresh_token
}, function(response) {
   access_token = response.access_token;
});

// 4. 로그아웃: Refresh Token 삭제 (서버에 블랙리스트 기록)

 

웹 인증은 보안, 성능, 확장성을 모두 고려해야 한다. 쿠키는 오래된 방식이지만 여전히 사용되고, 세션은 전통적인 웹에서 안전한 선택지이며, 토큰은 현대적인 API/모바일 환경에 가장 적합하다. 당신의 프로젝트 특성에 따라 세 가지 중 가장 적절한 방식을 선택하되, 보안을 절대 간과해서는 안 된다는 점을 잊지 말자.