JWT란? 로그인 인증을 가장 쉽게 이해하는 방법
웹사이트에 로그인할 때마다 우리는 자연스럽게 아이디와 비밀번호를 입력합니다.
하지만 로그인 이후에도 페이지를 새로고침하거나 다른 메뉴로 이동해도, 서버는 여전히 “당신이 로그인한 사용자임”을 인식하죠.
이건 단순한 마법이 아닙니다.
서버와 클라이언트(브라우저)가 서로 ‘인증 상태를 유지하는 약속’을 맺고 있기 때문이에요.
이 약속을 안전하고 효율적으로 관리하기 위해 현대 웹에서는 JWT(Json Web Token)이라는 기술을 사용합니다.
JWT는 복잡한 로그인 세션을 대체하고, 다중 서버 환경이나 모바일·웹 통합 서비스에서도 손쉽게 인증을 처리할 수 있도록 해주는 표준 토큰 기반 인증 방식입니다.
쉽게 말해, “누가 로그인했는지”를 담은 디지털 신분증 같은 역할을 합니다.

1. 사용자 인증이란? - 로그인과 토큰의 관계
현대의 대부분 웹사이트와 앱에는 로그인 기능이 있습니다.
로그인은 단순히 “문을 열고 들어가는 절차”가 아니라, 사용자를 식별하고 신뢰할 수 있도록 하는 인증(Authentication) 과정입니다.
로그인 과정에서 사용자는 보통 아이디(ID)와 비밀번호(Password)를 입력합니다.
서버는 이 정보를 데이터베이스(DB)에 저장된 값과 비교해, “이 사용자가 진짜 등록된 회원이 맞는가?”를 확인합니다.
인증에 성공하면 서버는 이후 요청에서 이 사용자를 ‘인증된 상태’로 인식합니다
그런데 여기서 중요한 질문이 하나 생깁니다.
“사용자가 로그인에 성공한 다음, 서버는 어떻게 그 사실을 기억할까?”
🔷 전통적인 방식: 세션(Session) 기반 인증
초기의 웹 서비스들은 세션(Session) 을 사용했습니다.
서버는 로그인에 성공한 사용자에게 고유한 세션 ID를 발급하고, 이 값을 서버 메모리나 데이터베이스에 저장합니다.
그리고 사용자의 브라우저에는 쿠키(Cookie) 형태로 세션 ID가 담깁니다.
이후 사용자가 페이지를 이동하거나 새 요청을 보낼 때마다 브라우저는 자동으로 쿠키를 서버에 함께 보내고, 서버는 세션 ID를 조회하여 “아, 이건 로그인된 사용자구나” 하고 인식합니다.
하지만 이 방식에는 치명적인 한계가 있습니다.
▸ 서버가 모든 세션 정보를 직접 저장해야 해서 메모리 사용량이 급격히 증가
▸ 여러 대의 서버(분산 환경)에서 세션 동기화를 해야 해서 관리 복잡도 증가
▸ 모바일 앱, REST API, 마이크로서비스 구조에서는 확장성 한계
즉, 세션 방식은 단일 서버 환경에서는 단순하고 효율적이지만, 대규모 서비스나 클라우드 환경에서는 점점 부담스러운 방식이 되어버렸습니다.
🔷 새로운 접근: 토큰 기반 인증 (Token-based Authentication)
이 문제를 해결하기 위해 등장한 개념이 토큰(Token) 입니다.
토큰 기반 인증에서는 서버가 로그인 성공 시 사용자 정보를 담은 “토큰”을 발급하고, 그 토큰을 이후 요청마다 클라이언트가 직접 전송합니다.
서버는 요청을 받을 때마다 토큰을 검증하여 “이 사용자가 유효한 토큰을 가진 사람인가?”만 판단합니다.
즉, 서버는 세션 정보를 따로 저장하지 않아도 인증 상태를 유지할 수 있게 된 거죠.
이 방식의 장점은 명확합니다.
▸ 서버 부하가 줄어듦 (상태 저장 불필요)
▸ 모바일·웹·API 모두 동일한 인증 구조 사용 가능
▸ 분산 서버 환경에서도 확장성 뛰어남
토큰은 일종의 “인증 증명서” 역할을 하며, 사용자가 누구인지, 언제 발급됐는지, 언제 만료되는지를 포함할 수 있습니다.
🔷 대표적인 표준: JWT (JSON Web Token)
이제 우리가 오늘 집중할 주제, JWT(Json Web Token)가 등장합니다.
JWT는 이름 그대로 JSON 형식의 데이터를 웹 표준 방식으로 인코딩한 토큰입니다.
즉, 사용자 인증 정보를 안전하게 담아 문자열 형태로 주고받는 약속된 형식이죠.
서버는 로그인 성공 시 JWT를 생성해 클라이언트에게 전달하고, 클라이언트는 이 토큰을 HTTP 요청 헤더에 함께 포함시켜 보냅니다.
서버는 그 안의 정보를 해독(verify)하여, “이 요청이 인증된 사용자에게서 온 것인지”를 판단합니다.
간단히 비유하자면 이렇습니다
세션은 “서버에 맡겨둔 신분증”이라면,
JWT는 “사용자가 직접 들고 다니는 신분증”입니다.
JWT는 가볍고, 빠르며, 언어나 플랫폼에 상관없이 동작합니다.
그래서 오늘날에는 웹 프론트엔드, 모바일 앱, 마이크로서비스, IoT 환경까지 모두 이 JWT 기반 인증 구조를 채택하고 있습니다
2. JWT 구조와 동작 원리 한눈에 보기
로그인 후 서버가 발급하는 JWT(Json Web Token)는 언뜻 보면 단순한 문자열처럼 보이지만, 그 안에는 사용자 신원을 증명하는 핵심 정보가 담겨 있습니다.
예를 들어 이런 형태의 토큰이 있습니다
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOjEyMywiZW1haWwiOiJ0ZXN0QGVtYWlsLmNvbSJ9.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
길고 복잡해 보이지만, 사실 이 문자열은 단순히 세 부분으로 나뉜 구조입니다.
점(.)으로 구분된 세 구간이 각각 JWT의 세 가지 핵심 요소를 의미합니다.
| 구분 | 이름 | 설명 |
| ① | Header | 토큰의 형식과 암호화 알고리즘을 명시합니다. 예: HS256, RS256 |
| ② | Payload | 사용자 정보와 인증 관련 데이터를 담습니다. 예: userId, email, exp(만료시간) |
| ③ | Signature | 토큰이 변조되지 않았음을 확인하는 디지털 서명입니다. |
🔷 Header — “이 토큰은 어떤 방식으로 암호화되었는가”
Header는 매우 간단한 JSON 객체입니다.
보통 아래처럼 토큰의 타입(JWT)과 서명 알고리즘(HS256 등)이 명시되어 있습니다.
{
"alg": "HS256",
"typ": "JWT"
}
이는 서버에게 “이 토큰은 HS256 알고리즘으로 서명되었어”라고 알려주는 역할을 합니다.
🔷 Payload — “누가 로그인했는가”를 담는 부분
Payload에는 실제 인증과 관련된 핵심 데이터가 들어갑니다.
이 데이터를 클레임(Claim)이라고 부릅니다.
{
"userId": 123,
"email": "test@email.com",
"exp": 1708009422
}
▸ userId, email 같은 값은 애플리케이션에서 사용자가 누구인지 식별하는 정보
▸ exp는 만료 시간(Expiration Time)으로, 토큰이 언제까지 유효한지 나타냅니다.
이 부분은 Base64로 인코딩 되어 있기 때문에 누구나 디코딩할 수 있지만, 서명(Signature) 이 없으면 위조할 수 없습니다.
즉, 보안은 “암호화”가 아니라 “검증”으로 유지됩니다
🔷 Signature — “이 토큰이 진짜임”을 증명하는 서명
마지막 Signature는 서버가 비밀키(Secret Key)를 이용해 생성한 서명입니다.
JWT의 핵심이 바로 이 부분에 있습니다.
Signature는 다음 수식으로 만들어집니다
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
즉, 헤더와 페이로드를 합쳐 비밀키로 암호화한 뒤, 그 결과를 토큰의 세 번째 부분에 붙인 것입니다.
서버는 나중에 토큰을 검증할 때 같은 방법으로 Signature를 다시 계산해 보고, 결과가 동일하면 “이 토큰은 변조되지 않았다”고 판단합니다.
✔️ 실제 요청에서 JWT가 사용되는 모습
클라이언트는 다음과 같이 HTTP 헤더에 JWT를 포함해 서버로 요청을 보냅니다.
GET /user/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
서버는 요청을 받을 때,
1. Authorization 헤더에서 토큰을 추출하고
2. 서명을 검증한 뒤(jwt.verify)
3. 유효한 경우에만 요청을 처리합니다.
이 과정 덕분에 서버는 세션을 저장하지 않아도 인증 상태를 유지할 수 있습니다.
즉, 서버는 “무상태(stateless)” 인증 시스템을 구현할 수 있는 것입니다.
3. JWT를 이용한 로그인·로그아웃 구현 흐름
앞서 JWT의 개념과 구조를 이해했다면, 이제 실제로 로그인 과정에서 JWT가 어떻게 사용되는지 단계별로 살펴볼 차례입니다.
JWT는 로그인 과정에서 사용자의 인증 정보를 담고, 이후 요청마다 “이 사용자가 인증된 사람임”을 증명하는 역할을 합니다.
즉, 사용자가 한 번 로그인하면, 그 뒤로는 매번 아이디와 비밀번호를 다시 입력할 필요가 없게 만드는 것이죠.
아래는 JWT 기반 로그인·로그아웃의 전형적인 흐름입니다
🔷 1단계. 로그인 요청 - 서버가 토큰을 발급하는 순간
사용자가 로그인 폼에서 이메일과 비밀번호를 입력하면, 브라우저(또는 모바일 앱)는 서버로 다음과 같은 요청을 보냅니다.
POST /login
Content-Type: application/json
{
"email": "test@email.com",
"password": "1234"
}
서버는 데이터베이스(DB)에서 해당 이메일을 찾고, 비밀번호가 일치하는지 확인합니다.
인증이 성공하면, 서버는 그 사용자를 대표하는 JWT 토큰을 생성해 반환합니다.
// Node.js 예시
import jwt from "jsonwebtoken";
const token = jwt.sign(
{ userId: 123, email: "test@email.com" },
process.env.JWT_SECRET, // 서버만 알고 있는 비밀키
{ expiresIn: "1h" } // 토큰 만료 시간 (1시간)
);
이렇게 만들어진 토큰은 사용자 정보가 암호화된 서명(Signature) 형태로 포함되어 있으며, 외부에서 임의로 변조하거나 위조하기 어렵습니다.
🔷 2단계. 토큰 저장 - 브라우저에 신분증을 보관하다
클라이언트(브라우저)는 서버로부터 받은 토큰을 localStorage나 쿠키(Cookie)에 저장합니다.
// 예: localStorage에 저장
localStorage.setItem("token", token);
이때 주의할 점은, 보안이 중요한 서비스에서는 쿠키 + HTTPS + HttpOnly 옵션을 사용하는 것이 더 안전하다는 점입니다.
왜냐하면 localStorage는 자바스크립트 코드에서 접근할 수 있어, XSS(스크립트 삽입 공격)에 노출될 수 있기 때문입니다.
🔷 3단계. 인증이 필요한 요청 — 토큰을 함께 전송하기
로그인 후, 사용자가 “내 정보 보기” 페이지를 요청한다고 가정해 봅시다.
이제 브라우저는 요청을 보낼 때, 저장해 둔 JWT를 HTTP 헤더에 포함해 전송합니다.
GET /user/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
서버는 이 헤더를 읽고 토큰을 추출합니다.
이 토큰 안에는 이미 userId나 email 같은 정보가 담겨 있으므로, 서버는 굳이 세션을 조회하지 않아도 “이 요청이 어떤 사용자에게서 왔는지” 바로 확인할 수 있습니다.
🔷 4단계. 서버 검증 - 토큰이 진짜인지 확인하기
서버는 jwt.verify() 함수를 이용해 토큰의 서명을 검증합니다.
이 과정에서 비밀키(JWT_SECRET)가 일치해야만 토큰이 유효하다고 인정됩니다.
// Node.js 예시
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.userId); // 인증된 사용자 ID
} catch (err) {
console.error("유효하지 않은 토큰입니다.");
}
검증에 성공하면 서버는 요청을 정상 처리하고, 실패하면 401 Unauthorized 오류를 반환합니다.
이 구조 덕분에 서버는 별도의 세션 저장소 없이도 안전하고 확장성 있는 인증 로직을 유지할 수 있습니다.
🔷 5단계. 로그아웃 - 토큰을 버리고 만료 기다리기
JWT는 서버에 저장되지 않는 인증 정보이기 때문에, 로그아웃은 매우 단순합니다.
그저 클라이언트가 저장된 토큰을 삭제하면 됩니다.
// 로그아웃 예시
localStorage.removeItem("token");
서버 입장에서는 “토큰이 더 이상 전송되지 않으면 인증이 해제된 것”으로 인식합니다.
또한 JWT는 발급 시 지정된 만료 시간(expiresIn)이 지나면 자동으로 효력을 잃습니다.
즉, 토큰이 만료되면 로그인 상태도 자연스럽게 해제됩니다.
4. 보안적으로 안전하게 JWT를 사용하는 방법
JWT는 로그인 인증을 간편하게 만들어주는 강력한 도구입니다.
하지만, 아무리 좋은 기술이라도 잘못 사용하면 보안이 쉽게 뚫릴 수 있습니다.
특히 JWT는 사용자가 직접 들고 다니는 “디지털 신분증” 역할을 하기 때문에, 이 신분증이 유출되거나 변조되면 심각한 보안 문제가 발생합니다.
그래서 JWT를 사용할 때는 반드시 아래의 세 가지 원칙을 지켜야 합니다
🔷 1. 비밀키(JWT_SECRET)는 절대 외부에 노출 금지
JWT는 내부적으로 서명(Signature)을 포함합니다.
서버는 토큰을 만들 때 JWT_SECRET이라는 비밀 키(Secret Key)를 사용해 서명하고, 검증할 때도 같은 키로 유효성을 확인합니다.
즉, 이 비밀키는 토큰의 신뢰성을 보장하는 핵심 열쇠입니다.
만약 누군가가 이 키를 알아낸다면, 누구든지 위조된 JWT를 만들 수 있습니다.
그 결과, 인증되지 않은 사용자가 “로그인된 사용자”처럼 행동할 수도 있죠.
따라서 반드시 다음과 같이 관리해야 합니다:
▸ .env 파일에 JWT_SECRET을 저장하고, GitHub 등 외부 저장소에 올리지 않기
▸ 운영 서버에서는 환경 변수(Environment Variable)로 주입
▸ 개발과 운영 환경의 Secret Key는 절대 동일하게 사용하지 않기
🔷 2. 토큰 만료 시간을 짧게 설정하기
JWT는 한 번 발급되면 서버가 강제로 폐기할 수 없습니다. (세션 방식과 달리 서버가 “저장”하지 않기 때문이죠.)
따라서 토큰이 탈취당하면, 만료될 때까지 공격자가 계속 사용할 수 있습니다.
이 문제를 줄이기 위해 토큰의 만료 시간(expiresIn)을 짧게 설정해야 합니다.
예를 들어:
▸ Access Token → 1시간 이내 (보통 15분~2시간 사이)
▸ Refresh Token → 7일~14일 정도로 길게 설정
Access Token이 만료되면, Refresh Token을 이용해 새로 발급받는 구조를 쓰면 사용자는 불편하지 않으면서도, 보안은 한층 강화됩니다.
const accessToken = jwt.sign(payload, secret, { expiresIn: "1h" });
const refreshToken = jwt.sign(payload, secret, { expiresIn: "7d" });
🔷 3. HTTPS를 사용하여 전송 암호화
JWT는 클라이언트가 서버로 전송할 때, 보통 HTTP 헤더(Authorization)에 포함됩니다.
문제는 HTTP는 평문(Plain Text)으로 통신하기 때문에, 네트워크 중간에서 누군가가 패킷을 가로채면 토큰이 그대로 노출될 수 있습니다.
이건 마치 신분증을 복사본 없이 택배로 보내는 것과 비슷하죠 - 위험합니다.
따라서 JWT는 반드시 HTTPS(SSL/TLS) 환경에서만 주고받아야 합니다.
HTTPS는 통신 전 과정을 암호화하기 때문에, 중간에 누가 데이터를 훔쳐보더라도 내용을 해독할 수 없습니다.
Tip:
로컬 개발 환경에서도 mkcert나 ngrok을 이용해 HTTPS 테스트 가능
프록시 서버(Nginx, Cloudflare)에서 SSL을 설정하는 것도 좋은 방법입니다.
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'5. IT기술노트 > 인프라&개발' 카테고리의 다른 글
| PASETO 이해하기: JWT를 대체하는 안전한 토큰 인증 (0) | 2025.10.27 |
|---|---|
| 로그인 시스템의 핵심: Access Token과 Refresh Token 쉽게 설명하기 (0) | 2025.10.26 |
| CORS란 무엇인가? - 브라우저 보안의 핵심과 실무 이해 (0) | 2025.10.20 |
| ESLint: 자바스크립트 코드 품질을 손쉽게 지키는 첫걸음 (0) | 2025.10.19 |
| SSR(Server Side Rendering): 왜, 어떻게, 그리고 언제 써야 할까? (0) | 2025.10.16 |