본문 바로가기

React

React에서 쿠키 기반 사용자 설정 저장하기

다크 모드, 언어, 글자 크기 같은 사용자 설정은 초기에 서버에서 바로 반영되면 깜빡임 없이 안정적인 UX를 제공합니다. 쿠키는 서버와 클라이언트가 모두 접근할 수 있어 SSR과 초기 렌더 동기화에 유리합니다. 이 글은 React에서 쿠키로 사용자 설정을 안전하고 간단하게 저장하는 실무 패턴을 정리합니다.

1. 쿠키를 쓰는 이유와 쓰지 말아야 할 때

- 쓰면 좋은 경우: 서버 렌더링(Next.js 등)에서 초기 설정 반영, 서브도메인 간 공유(domain 옵션), 백엔드에서 설정 읽기.

- 피해야 할 경우: 민감 정보(토큰/개인정보), 큰 데이터(쿠키는 4KB 제한), 탭 간 실시간 동기화가 필요한 경우(쿠키는 스토리지 이벤트 없음).

2. 쿠키 기본기(속성과 한계)

- expires/max-age: 만료 설정입니다. 없으면 세션 쿠키가 됩니다.

- path/domain: 적용 범위를 지정합니다. 불필요한 domain 설정은 충돌/보안 이슈를 만들 수 있습니다.

- SameSite: Lax(권장), Strict, None(+Secure 필수). CSRF 위험을 고려해 설정합니다.

- Secure: HTTPS에서만 전송합니다. 운영환경에서는 반드시 사용합니다.

- HttpOnly: 자바스크립트에서 읽을 수 없습니다. 프론트에서 쓰는 설정 쿠키에는 적용 불가합니다.

- 한계: 도메인당 쿠키 개수 제한(브라우저별 상이), 쿠키당 4KB 제한.

3. 최소 유틸 함수로 시작하기

쿠키 접근은 document.cookie 문자열 파싱이 기본입니다. 안전하게 인코딩/옵션을 처리하는 헬퍼를 만듭니다.

// cookie.js
export function getCookie(name) {
  const match = document.cookie
    .split('; ')
    .find((row) => row.startsWith(name + '='));
  return match ? decodeURIComponent(match.split('=')[1]) : null;
}

export function setCookie(name, value, { days = 180, path = '/', sameSite = 'Lax', secure, domain } = {}) {
  const parts = [];
  parts.push(`${name}=${encodeURIComponent(value)}`);
  if (days) {
    const date = new Date();
    date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
    parts.push(`Expires=${date.toUTCString()}`);
  }
  parts.push(`Path=${path}`);
  if (domain) parts.push(`Domain=${domain}`);
  if (sameSite) parts.push(`SameSite=${sameSite}`);
  const isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
  if (secure || isHttps) parts.push('Secure');
  document.cookie = parts.join('; ');
}

export function deleteCookie(name, { path = '/', domain } = {}) {
  setCookie(name, '', { days: -1, path, domain });
}

4. React 훅: useUserPrefs로 읽고 쓰기

초기 렌더 시 쿠키를 읽고, 사용자가 바꾸면 쿠키를 갱신합니다. JSON으로 한 번에 묶되 4KB를 넘지 않도록 필드만 최소화합니다.

// useUserPrefs.js
import { useEffect, useMemo, useState, useCallback } from 'react';
import { getCookie, setCookie } from './cookie';

const COOKIE_KEY = 'prefs_v1'; // 버전 포함 키로 추후 마이그레이션 대비

const getSystemTheme = () => (
  window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
);

export function useUserPrefs() {
  const [prefs, setPrefs] = useState(() => {
    try {
      const raw = typeof document !== 'undefined' ? getCookie(COOKIE_KEY) : null;
      if (raw) return JSON.parse(raw);
    } catch {}
    return {
      theme: 'light',
      lang: 'en',
    };
  });

  // 초기 시스템 테마/브라우저 언어 반영(쿠키 없을 때만)
  useEffect(() => {
    if (!getCookie(COOKIE_KEY)) {
      setPrefs((p) => ({
        theme: getSystemTheme(),
        lang: navigator.language?.slice(0, 2) || 'en',
      }));
    }
  }, []);

  // DOM 반영 + 쿠키 저장
  useEffect(() => {
    try {
      document.documentElement.dataset.theme = prefs.theme; // CSS에서 [data-theme="dark"] 사용
      setCookie(COOKIE_KEY, JSON.stringify(prefs), { days: 180, sameSite: 'Lax' });
    } catch {}
  }, [prefs]);

  const setTheme = useCallback((theme) => setPrefs((p) => ({ ...p, theme })), []);
  const setLang = useCallback((lang) => setPrefs((p) => ({ ...p, lang })), []);

  return useMemo(() => ({ prefs, setTheme, setLang }), [prefs, setTheme, setLang]);
}

5. 컴포넌트 예시

토글로 테마를 바꾸고, 셀렉트로 언어를 선택합니다.

// Settings.jsx
import React from 'react';
import { useUserPrefs } from './useUserPrefs';

export default function Settings() {
  const { prefs, setTheme, setLang } = useUserPrefs();

  return (
    <div>
      <h4>사용자 설정</h4>
      <label>
        <input
          type="checkbox"
          checked={prefs.theme === 'dark'}
          onChange={(e) => setTheme(e.target.checked ? 'dark' : 'light')}
        />
        다크 모드
      </label>

      <div style={{ marginTop: 8 }}>
        <select value={prefs.lang} onChange={(e) => setLang(e.target.value)}>
          <option value="en">English</option>
          <option value="ko">한국어</option>
          <option value="ja">日本語</option>
        </select>
      </div>
    </div>
  );
}

6. SSR/Next.js에서 초기 깜빡임 방지

서버에서 쿠키로 테마/언어를 읽어 HTML에 바로 반영하면 초기 깜빡임을 줄일 수 있습니다.

// app/layout.js (Next.js App Router 예시)
import { cookies } from 'next/headers';

export default function RootLayout({ children }) {
  const store = cookies();
  let theme = 'light';
  let lang = 'en';
  try {
    const raw = store.get('prefs_v1')?.value;
    if (raw) ({ theme, lang } = JSON.parse(raw));
  } catch {}

  return (
    <html lang={lang} data-theme={theme}>
      <body>{children}</body>
    </html>
  );
}
// 서버 액션/라우트 핸들러에서 설정 갱신 (Next.js App Router)
import { cookies } from 'next/headers';

export async function POST(req) {
  const { theme, lang } = await req.json();
  const value = JSON.stringify({ theme, lang });
  cookies().set('prefs_v1', value, {
    path: '/',
    maxAge: 60 * 60 * 24 * 180,
    sameSite: 'lax',
    secure: true,
  });
  return new Response(null, { status: 204 });
}

Express 등의 SSR 서버라면 Set-Cookie 헤더를 사용합니다.

// Express 예시
app.post('/prefs', (req, res) => {
  const { theme, lang } = req.body;
  res.cookie('prefs_v1', JSON.stringify({ theme, lang }), {
    path: '/',
    maxAge: 1000 * 60 * 60 * 24 * 180,
    sameSite: 'lax',
    secure: true,
  });
  res.sendStatus(204);
});

7. 만료 전략과 버전 관리

- 접근 시 재발급: 사용자가 상호작용할 때마다 max-age를 갱신해 선호가 오래 유지되도록 합니다.

- 버전 필드/키: prefs_v1처럼 버전을 포함해 구조 변경 시 마이그레이션을 쉽게 합니다.

- 크기 관리: 불필요 필드는 저장하지 말고, 배열/객체는 납작하게 최소화합니다.

8. 보안과 규정 체크리스트

- 민감정보 금지: 토큰/개인정보는 쿠키에 저장하지 않습니다.

- XSS 대비: 값은 반드시 encodeURIComponent/JSON 사용, 외부 스크립트 삽입을 막습니다.

- SameSite=Lax + Secure: 운영환경 기본값으로 권장합니다.

- 동의(Consent): 지역 규정(GDPR/쿠키 법)에 따라 설정 쿠키도 고지/동의를 고려합니다.

9. 디버깅 팁

- DevTools > Application > Cookies에서 값/속성을 확인합니다.

- document.cookie로 런타임 값을 빠르게 확인합니다.

- Set-Cookie가 차단되는 경우: 도메인/경로/SameSite/시간/HTTPS 조건을 점검합니다.

- 탭 간 동기화가 필요하면 BroadcastChannel 또는 localStorage+storage 이벤트로 보완합니다.

10. 결론

사용자 설정은 쿠키로 서버/클라이언트를 연결하면 초기 렌더 품질이 좋아집니다. 위 유틸/훅 패턴과 SSR 연동을 적용하고, SameSite/보안/만료 정책을 기본값으로 습관화하면 실무에서 안정적으로 운영할 수 있습니다.