모듈 상단 상수에 쿠키를 읽으면 왜 안 되는가
JS Module Evaluation Timing
난이도: ★★☆☆☆
연관 노트: router를 useMemo 의존성에 넣으면 라우트가 바뀌어도 재계산이 안 되는 이유
핵심 요약
모듈 최상위 상수는 JS 번들 모듈이 로드될 때 한 번만 실행되고, useMemo는 컴포넌트가 렌더될 때 실행되기 때문에 쿠키 같은 런타임 값은 useMemo 안에서만 제때 읽힌다.
왜 헷갈렸나
“buildMenuUrl이든 buildMenuUrlWithParams든 상수 초기화 시점에 평가되니까 어차피 의미없다”는 설명을 들었는데, 함수를 두 개로 나누거나 이름을 바꾸는 것과 무슨 차이인지 바로 연결이 안 됐다.
문제가 “어떤 함수를 쓰느냐”가 아니라 “어느 위치에서 호출하느냐”임을 몰랐다.
메커니즘
JS 모듈은 처음 import될 때 모듈 스코프 전체가 위에서 아래로 1회 실행된다. 이후 같은 모듈이 다시 import돼도 이미 캐시된 결과를 반환할 뿐 재실행되지 않는다.
번들 로드
└─ Header.tsx 모듈 평가
└─ const NAV_MENUS = [{ to: buildMenuUrl("/") }]
└─ buildMenuUrl() 실행 → 이 시점 쿠키를 읽음
※ 이후 쿠키가 바뀌어도 NAV_MENUS는 고정
반면 useMemo는 컴포넌트 렌더 사이클에 속한다:
사용자가 페이지 이동 (리렌더)
└─ Navigation 컴포넌트 실행
└─ useMemo(() => [{ to: buildMenuUrl("/") }], [isActive])
└─ buildMenuUrl() 실행 → 이 시점 쿠키를 읽음
※ 렌더마다 최신 쿠키 반영 가능
해결 패턴
// ❌ 모듈 최상위 — 로드 시점에 고정
const MENUS = [
{ text: "홈", to: buildMenuUrl("/") },
];
// ✅ useMemo — 렌더 시점에 평가
const menus = useMemo(() => [
{ text: "홈", to: buildMenuUrl("/") }, // 렌더마다 최신 쿠키 읽음
], []);
dependency 배열이 []여도 괜찮은 경우: 쿠키는 페이지 로드 시 한 번 세팅되고 세션 중 변경되지 않는 구조라면, 컴포넌트 마운트 시점(= 첫 렌더)에 읽어도 충분하다.
다음에 이 상황을 만나면
“상수에 함수 호출 결과를 담았는데 값이 안 바뀐다” → 즉시 위치 확인:
- 모듈 최상위인가? → 렌더 사이클(useMemo, 컴포넌트 body)로 이동
-
useMemo의 dependency가 비어있는가? → 런타임 값이 렌더 간에 바뀌는지 확인 - 읽으려는 값이 모듈 로드 이후에 세팅되는가? (쿠키, 토큰, localStorage)
커넥팅 닷
← 선행 개념 (이걸 알아야 이해된다)
- JS Module System (ESM) — 모듈은 처음 import 시 1회 평가되고 이후 캐시됨. 이 전제를 몰라야 “왜 상수가 안 바뀌냐”를 이해 못 함
- router를 useMemo 의존성에 넣으면 라우트가 바뀌어도 재계산이 안 되는 이유 — useMemo가 재실행되지 않는 또 다른 패턴 (dependency 오판)
→ 확장 개념 (여기서 더 나아가면)
- React 렌더 사이클 전체 — useMemo, useCallback, useEffect가 각각 어느 시점에 실행되는지 정확히 구분하는 것으로 연결
- SSR/SSG에서의 모듈 평가 — 서버에서 번들이 로드될 때도 동일한 1회 평가 원칙이 적용됨.
typeof window === "undefined"가드가 필요한 이유
↔ 같은 원리가 적용되는 곳
- sessionStorage에서 읽어온 값이 useCallback 안에서 이전 값을 보여주는 이유 — “초기화 시점에 고정된 값”이라는 공통 원인
- 서버와 브라우저의 ‘오늘’은 다르다 — “평가 시점과 환경이 어긋나면 값이 어긋난다”는 공통 구조. 모듈 로드 vs 렌더, 서버 vs 브라우저
- 싱글턴 패턴 부작용 — 모듈 캐시 = 사실상 싱글턴. 테스트에서 모듈 상태가 공유되어 테스트 간 오염 발생
- useLayoutEffect가 SSR에서 경고를 내는 이유와 useIsomorphicLayoutEffect —
useIsomorphicLayoutEffect를 모듈 레벨에 선언하는 이유가 “모듈 로드 시 1번 평가, 이후 고정”이라는 동일한 원리. 목적만 다름(런타임 값 고정 vs Rules of Hooks 만족)
참고
- MDN — JavaScript Modules — 모듈 평가 및 캐싱 동작 설명