개발로그

카드 게임 제작 ( 3 ) - JWT 토큰은 json web token token이다

ddony8128 2026. 2. 16. 10:31

 카드 게임 제작 기록 처음부터 보기 :  https://helloworld.ai.kr/14

 

카드 게임 제작 ( 1 ) - 회고

게임 링크 : cardgame.perfect.ai.kr 개발 기간 : 2025.10.28 ~ 2025.11.27총 소요 시간 : 110시간Lovable에게 감탄 ( UI 작업 )https://blog.helloworld.ai.kr/15JWT 토큰은 json web token token이다 ( 로그인 구현 )https://blog.helloworl

helloworld.ai.kr

 

게임 링크 : cardgame.perfect.ai.kr

 

Magician Duel

 

cardgame.perfect.ai.kr

 

이 시점의 소요 시간 : 36시간 / 110시간

  • DB 스키마 설계 / 카드 정보 json 작성
  • DB 초기화, seed 작업
  • Supabase와 서버 연동
  • 로그인 방식과 웹 스토리지에 대한 이해

 

작업 과정

이번 프로젝트에서 반복적으로 사용한 루틴이 있다.

1. GPT와 의논하며 문서 작성

기획이나 구현 방법을 GPT와 계속 질의응답하면서 결정했다.

  • 모르는 개념 정리
  • 구현 방식 비교
  • 선택지 정리

특히 이번에는 로그인 구현과 웹 스토리지 부분을 꽤 명확히 이해하게 됐다.

2. 커서에게 기획 던져주기

스펙이 어느 정도 정리되면 커서에게 통째로 넘긴다.

폴더 구조나 초기 뼈대를 혼자서 잡기 막막할 때가 많은데,
커서가 만들어준 구조를 보면 감이 좀 잡힌다.

3. 코드 이해 및 수정

코드를 한 줄씩 읽어가며

  • 합리적인지 판단
  • 모르는 부분은 GPT에게 질문
  • 이상한 부분 수정
  • 스펙 자체가 틀렸으면 설계 수정

4. 문서 수정

코드 변경 사항을 문서에 반영한다.


이 과정을 반복하며 DB 구성과 API 구현을 진행했다.
GPT님이 없었으면 3배는 걸렸을 것 같다.


로그인

예전에 로그인은 세션 방식으로만 구현해봤다.
찾아보니 요즘은 JWT(Json Web Token)를 많이 쓴다고 한다.

세션 방식

서버 DB에 로그인 상태 저장
서버가 로그인 상태를 직접 들고 있음
→ stateful

JWT 방식

인증 정보를 토큰에 담아 클라이언트가 보관
서버는 토큰 유효성만 검증
→ stateless

JWT 방식의 구조가 훨씬 가볍다.


JWT 구조

JWT는 세 부분으로 나뉜다.

헤더 : 어떤 알고리즘으로 서명했는지
페이로드 : 유저 정보, 만료 시간 등 실제 데이터
시그니처 : JWT_SECRET 값으로 서명한 검증값
(JWT_SECRET은 그냥 랜덤 값 생성해서 활용한다)

로그인 흐름은 다음과 같다.

서버가 JWT 발급 → 클라이언트가 저장 → 이후 모든 요청마다 JWT 첨부 → 서버가 매 요청마다 검증


JWT의 위험과 대응

JWT 토큰이 탈취되면 유저를 사칭할 수 있다.

대표적인 대응 방법 두 가지.

  • HttpOnly 쿠키

JWT를 HttpOnly 쿠키에 저장하면 JS 코드에서 접근할 수 없다.
→ XSS로 토큰이 유출될 가능성을 줄일 수 있다.

  • 짧은 만료 + Refresh Token

access token의 만료 기간은 짧게, refresh token은 길게 만든다.
필요할 때 access token을 재발급한다.
실무에서 거의 표준처럼 쓰이는 패턴이다.


OAuth

OAuth는 외부 플랫폼이 대신 유저를 인증해주는 방식이다.

흐름은 다음과 같다.

클라이언트 → 외부 플랫폼 로그인
외부 토큰을 서버로 전달
서버가 외부 플랫폼에 유효성 확인
서버가 자체 JWT 발급

즉 OAuth를 쓰더라도 내 서비스 내부에서는 결국 JWT를 쓰게 된다.


웹 스토리지

브라우저 저장소에 대한 내용도 한 번 정리했다.
그동안 캐시, 쿠키, 로컬스토리지 개념을 좀 혼용해서 알고 있었다.

Session Storage

새로고침을 해도 남아있지만 탭을 닫는 순간 사라진다.
활용처가 애매하다.
임시 폼 데이터 정도에 쓸 듯.


Local Storage

무난하게 쓰기 좋은 저장소.

브라우저를 닫았다가 다시 열어도 남아있다.
key-value 방식으로 텍스트 저장
도메인당 약 5MB 할당

주요 용도:

  • UI 설정
  • zustand persist
  • 민감하지 않은 데이터

IndexedDB

사실상 브라우저에 달린 데이터베이스라고 볼 수 있다.

수 MB ~ 수 GB
비동기 입출력
구조화 데이터 저장
이미지 저장 가능

오프라인 기능이나 대용량 캐시에 적합하다.


Cache Storage

서비스 워커와 함께 작동한다.

HTML, JS, 이미지 같은 정적 자원을 통째로 캐싱할 때 사용.
PWA에서 자주 쓰인다.


쿠키

서버가 설정하면 브라우저는 같은 도메인 요청마다 자동으로 첨부한다.

HttpOnly 옵션을 켜면 JS 코드에서 접근할 수 없다.
인증 토큰 보관 용도로서 중요하다.


타입 관련 문법

커서가 적어준 코드 중 인상 깊었던 타입스크립트 문법을 정리해둔다.
좀 겉멋 같은 느낌이 있다.

1. Pick

attrs: Pick<DeckRaw, "name"> & {
  main_cards: DeckList;
  cata_cards: DeckList;
}

특정 타입에서 필요한 속성만 선택적으로 추출한다.

결과 타입:

{
  name: string;
  main_cards: DeckList;
  cata_cards: DeckList;
}

2. typeof, keyof, as const

export const HttpStatus = {
  OK: 200,
  CREATED: 201,
  NOT_FOUND: 404,
} as const;

export type HttpStatusCode =
  typeof HttpStatus[keyof typeof HttpStatus];

핵심:

  • as const → 리터럴 타입 고정
  • keyof → 키 유니언
  • typeof T[K] → 값 유니언

결과:

200 | 201 | 404

당시에는 이게 무슨 문법이야... 라는 생각이었는데 javascript에 enum 클래스가 존재하지 않아서 이 문법이 굉장히 유용하다.
마치 관용구처럼 잘 활용하고 있다.

3. value is type

export function isDeckList(value: unknown): value is DeckList {
  …
}

사용자 정의 타입 가드.

이 함수가 true를 반환하면
value가 DeckList 타입임을 보장한다.

 

다음 단계

문서 작업, 데이터베이스 세팅, 로그인 구현을 완료했다.
이제 HTTP API들을 만들고 백엔드와 프론트엔드를 연결할 차례다.
다음 글 보러 가기 : https://helloworld.ai.kr/17