다크 모드, 언어, 글자 크기 같은 사용자 설정은 초기에 서버에서 바로 반영되면 깜빡임 없이 안정적인 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/보안/만료 정책을 기본값으로 습관화하면 실무에서 안정적으로 운영할 수 있습니다.
'React' 카테고리의 다른 글
| React에서 비동기 컴포넌트 로딩 순서 제어하기 (0) | 2026.07.02 |
|---|---|
| React 앱에서 브라우저 Payment Request API 통합하기 (1) | 2026.07.02 |
| React 앱에서 로컬 네트워크 프린터 연동하기 (0) | 2026.06.30 |
| React에서 이미지 스프라이트(Sprite) 기법 적용하기 (0) | 2026.06.30 |
| React 앱에서 사용자 입력 예측(Auto-complete) 시스템 구현 (0) | 2026.06.29 |