웹 사이트를 개발하다가 사용자가 실수로 브라우저 새로고침(F5)을 누르거나 페이지를 이동했을 때, 기껏 작성 중이던 폼 데이터나 다크 모드 설정값이 초기화되어 날아가는 문제를 겪어보신 적 있으신가요?

저 역시 초기 프로젝트에서 모든 상태를 React useState로만 관리했다가, 사용자가 브라우저를 껐다 켜거나 새로고침할 때마다 설정이 리셋되는 피드백을 받았습니다.

서버 데이터베이스(DB)에 일일이 저장하기에는 너무 자잘한 프론트엔드 설정값들(다크 모드 테마, 장바구니 임시 데이터, 팝업 '오늘 하루 보지 않기' 등)을 어디에 보관해야 할지 고민하다가 브라우저 저장소(Web Storage API)를 본격적으로 활용하게 되었습니다.

오늘은 브라우저의 대표적인 3대 저장소인 LocalStorage, SessionStorage, Cookie의 차이점과 실무에서 안전하게 활용했던 노하우를 공유해 보려고 합니다.

1. 3대 브라우저 저장소 핵심 비교: "어디에 무엇을 저장해야 할까?"

웹 브라우저가 제공하는 저장소는 수명(Lifecycle)과 용도에 따라 명확히 나뉩니다.

  • LocalStorage (로컬 스토리지): 브라우저 창을 닫아도 데이터가 영구적으로 유지됩니다. (용량 약 5MB) ➔ 다크 모드 테마, 사용자 맞춤 UI 설정에 적합
  • SessionStorage (세션 스토리지): 현재 탭(세션)이 열려 있는 동안에만 유지되며, 탭을 닫으면 즉시 삭제됩니다. ➔ 일회성 폼 입력 중간 저장, 결제 진행 단계 임시 데이터에 적합
  • Cookie (쿠키): 만료일을 설정할 수 있고 서버 요청 시 자동으로 헤더에 포함됩니다. (용량 약 4KB) ➔ 로그인 인증 토큰(HttpOnly) 등에 주로 사용

2. 실무 적용 1: LocalStorage로 다크 모드(Theme) 설정 영구 저장하기

사용자가 라이트 모드/다크 모드를 선택했을 때, 브라우저를 닫고 며칠 뒤에 다시 들어와도 그 설정을 기억하도록 localStorage를 적용했습니다.

TypeScript
 
// ⭕ 로컬스토리지에 테마 저장 및 불러오기 함수
const THEME_KEY = 'app_user_theme';

// 1. 테마 저장하기
export const saveTheme = (theme: 'light' | 'dark') => {
  try {
    localStorage.setItem(THEME_KEY, theme);
  } catch (error) {
    console.error('스토리지 저장 실패:', error);
  }
};

// 2. 테마 불러오기 (기본값: 'dark')
export const getSavedTheme = (): 'light' | 'dark' => {
  if (typeof window === 'undefined') return 'dark';
  const saved = localStorage.getItem(THEME_KEY);
  return saved === 'light' ? 'light' : 'dark';
};

3. 실무 적용 2: 객체(Object)나 배열 다룰 때 JSON.stringify 필수 사용

초보 시절 가장 많이 범했던 실수는 객체 데이터를 그대로 localStorage.setItem('user', { name: 'dev' })로 저장했던 것입니다.

브라우저 스토리지는 문자열(String)만 저장할 수 있기 때문에, 객체를 그냥 넣으면 [object Object]라는 깨진 문자열로 변환되어 저장됩니다.

JavaScript
 
const userProfile = { id: 1, name: '김개발', role: 'frontend' };

// ❌ 잘못된 저장 방식: [object Object]로 저장됨
localStorage.setItem('user', userProfile);

// ⭕ 올바른 저장 방식: JSON 문자열로 직렬화 후 저장
localStorage.setItem('user', JSON.stringify(userProfile));

// ⭕ 꺼내올 때: JSON.parse로 다시 자바스크립트 객체로 변환
const savedUser = JSON.parse(localStorage.getItem('user'));
console.log(savedUser.name); // '김개발' 정상 접근 가능!

localStorage.setItem 및 JSON.stringify

4. 실무 적용 3: 크롬 개발자 도구(Application 탭)로 데이터 확인하기

스토리지에 데이터가 제대로 들어갔는지 확인하려면 브라우저 개발자 도구의 Application 탭을 활용하면 됩니다.

  1. F12 개발자 도구 열기
  2. 상단 메뉴 중 Application (애플리케이션) 클릭
  3. 좌측 메뉴 Storage > Local Storage 클릭 후 내 도메인 선택

그러면 내가 코드로 저장한 Key와 Value가 실시간으로 표 형태로 직관적이게 표시되는 것을 확인할 수 있습니다.

local storage저장 확인

5. 결론 및 주의할 점

웹 스토리지는 사용하기 정말 편리하지만, 보안상 한 가지 중요한 원칙이 있습니다.

  • LocalStorage에는 절대 민감한 정보(비밀번호, 개인정보, 중요한 API 시크릿 키)를 저장하지 말 것!

XSS(Cross-Site Scripting) 공격에 노출될 경우 자바스크립트 코드로 쉽게 탈취될 수 있기 때문입니다. 따라서 UI 설정값, 캐시성 데이터, 비인증 임시 상태 위주로 활용하고, 중요한 인증 토큰은 안전한 HttpOnly Cookie를 활용하는 것이 모범 사례입니다.

웹 애플리케이션의 복잡도가 높아지면서 프론트엔드 보안의 중요성도 그 어느 때보다 커졌습니다. 보안 사고는 서비스의 신뢰도를 한순간에 무너뜨릴 뿐만 아니라 사용자의 개인정보 유출로 직결됩니다. 오늘은 프론트엔드 개발자가 반드시 알고 있어야 할 두 가지 핵심 공격 기법인 XSS와 CSRF의 원리를 파악하고, 이를 방어하기 위한 실무 전략을 알아보겠습니다.


1. XSS (Cross-Site Scripting): 내 사이트에 남이 짠 코드가?

XSS는 공격자가 악성 스크립트를 웹 페이지에 삽입하여, 다른 사용자의 브라우저에서 해당 스크립트가 실행되게 만드는 공격입니다.

1.1 공격의 위험성

스크립트가 실행되면 공격자는 사용자의 세션 쿠키를 가로채거나, 사용자를 대신해 게시글을 작성하고, 민감한 정보를 외부 서버로 전송할 수 있습니다.

1.2 프론트엔드 방어 전략

React와 같은 최신 프레임워크는 기본적으로 렌더링 시 데이터를 이스케이프(Escape) 처리하여 XSS를 방지합니다. 하지만 다음 상황에서는 위험에 노출됩니다.

  • dangerouslySetInnerHTML 사용 지양: HTML을 직접 렌더링해야 한다면 반드시 dompurify 같은 라이브러리로 살균(Sanitize) 과정을 거쳐야 합니다.
  • 사용자 입력값 검증: URL 쿼리 스트링이나 폼 입력값을 다룰 때 항상 유효성을 검사하세요.
  • CSP(Content Security Policy) 설정: 브라우저에게 "이 사이트에서 실행할 수 있는 스크립트 출처는 여기뿐이야"라고 알려주는 보안 헤더를 설정하세요.

2. CSRF (Cross-Site Request Forgery): 내가 안 보낸 요청이 서버로?

CSRF는 사용자가 자신의 의지와 무관하게 공격자가 의도한 행위(비밀번호 변경, 결제 등)를 특정 웹사이트에 요청하게 만드는 공격입니다. 사용자가 로그인되어 있는 브라우저의 '쿠키'를 이용하는 것이 핵심입니다.

2.1 공격 원리

사용자가 은행 사이트에 로그인된 상태에서 공격자가 만든 악성 사이트에 접속하면, 그 사이트 내부의 숨겨진 폼이 은행 서버로 '송금 요청'을 보냅니다. 브라우저는 은행 쿠키를 자동으로 함께 전송하므로, 서버는 사용자의 정상적인 요청으로 착각하게 됩니다.

2.2 프론트엔드 방어 전략

  • SameSite 쿠키 속성: 쿠키 설정 시 SameSite=Lax 또는 Strict를 사용하여 타 도메인에서의 쿠키 전송을 제한하세요.
  • CSRF 토큰: 서버에서 발급한 일회용 토큰을 요청 헤더에 담아 보내 서버가 검증하게 하세요.
  • Custom Header 사용: X-Requested-With 같은 커스텀 헤더를 요구하면 브라우저의 기본 폼 제출로는 요청을 보낼 수 없어 방어에 유리합니다.

3. 안전한 토큰 관리: LocalStorage vs Cookie

많은 개발자가 고민하는 주제입니다. "JWT(JSON Web Token)를 어디에 저장해야 할까요?"

3.1 LocalStorage (XSS에 취약)

  • 장점: 사용이 간편하고 CSRF 공격으로부터 자유롭습니다.
  • 단점: 자바스크립트로 접근이 가능하므로 XSS 공격에 노출되면 토큰이 그대로 탈취됩니다.

3.2 HttpOnly Cookie (CSRF에 취약)

  • 장점: 자바스크립트 접근이 불가능하여 XSS로부터 안전합니다.
  • 단점: 브라우저가 자동으로 요청에 포함하므로 CSRF 공격 대상이 됩니다.

최선의 선택: HttpOnly 및 Secure 속성이 적용된 쿠키를 사용하되, SameSite 속성과 CSRF 토큰을 병행하여 CSRF 공격을 방어하는 것이 가장 견고한 보안 모델로 평가받습니다.


4. HTTPS와 보안 헤더 설정

네트워크 레벨에서의 보안도 필수입니다.

  • HSTS (HTTP Strict Transport Security): 브라우저가 해당 사이트에 항상 HTTPS로만 접속하도록 강제합니다.
  • X-Frame-Options: 내 사이트가 다른 사이트의 <iframe> 안에 들어가는 것을 막아 클릭재킹(Clickjacking) 공격을 방어합니다.

5. 결론: 보안은 체크리스트가 아닌 프로세스입니다

프론트엔드 보안은 단 하나의 라이브러리 설치로 끝나지 않습니다. 코드를 짤 때마다 "이 입력값이 안전한가?", "이 요청이 신뢰할 수 있는가?"를 끊임없이 질문해야 합니다.

오늘의 요약:

  1. XSS 방어: 데이터 이스케이프를 믿되, HTML 직접 삽입 시에는 반드시 살균하세요.
  2. CSRF 방어: SameSite 쿠키와 커스텀 헤더/토큰을 활용하세요.
  3. 저장소 선택: 민감한 정보는 자바스크립트가 만질 수 없는 곳에 두세요.
  4. CSP 도입: 보안 헤더를 통해 브라우저의 방어 능력을 극대화하세요.

+ Recent posts