
최근 리액트에서 0을 다루는 법 때문에 당황했던 경험을 공유하고자 한다. \
문제는 "사이드바를 펼친 상태에서 사이드바 상단 로고 옆에 0이 표시되는 것"이었다.
원인은 사이드바의 접힘 상태를 관리하는 훅 useSidebarCollapsed의 구현 방식에 있었다. 해당 훅은 접힘 상태를 boolean으로 관리하고 있었고, 이 값을 localStorage와 연동하는 로직이 있었다. localStorage는 모든 값을 문자열로만 저장하므로 되읽을 때 JSON.parse로 복원하는데, JSON.parse(raw) as T의 as T는 컴파일러를 향한 선언일 뿐 런타임 검증이 없다. 그래서 과거 버전이 localStorage에 남긴 숫자 0이 검증 없이 복원 과정을 통과했고, 소비처에는 boolean이 아닌 숫자가 그대로 흘러들었다.
사이드바에는 접혔을 때만 렌더링되는 토글 버튼이 있는 구조였다.
{collapsed && <ToggleButton/>}
접혔을 때는 아래와 같은 형태로 구성되어 있다.

여기서 리액트의 특성과 맞물려 문제가 발생했다. 0은 자바스크립트에서 falsy이므로, 사이드바 폭을 정하는 삼항
collapsed ? 'w-14' : 'w-56'
은 펼침으로 간다. 그런데 같은 0이
{collapsed && <ToggleButton/>}
에서는 &&의 반환값인 숫자 0으로 남고, 리액트는 이를 텍스트로 그린다.
리액트는 boolean(false/true), null, undefined는 렌더하지 않고 무시하지만, 같은 falsy라도 숫자 0과 NaN은 렌더 가능한 값으로 취급해 텍스트로 그린다.
핵심은 0이 "false가 되어" 사라지는 게 아니라, falsy이지만 값은 숫자 0으로 남아 렌더된다는 점이다.
false였다면 false && JSX → false가 되어 리액트가 지웠을 것이다.
그래서 "펼친 상태에서 사이드바 상단 로고 옆에 0"이라는 조합이 나오게 된 것이다.
결과적으로 고치는 것은 쉬웠다. 처음에는 헤더 쪽을 {!!collapsed && ...}로 막는 방법도 있었지만, 그 방식은 그 한 줄만 막을 뿐 폭을 정하는 삼항 등 collapsed를 쓰는 다른 소비처에는 여전히 숫자 0이 흐른다. 저장소는 신뢰 경계이므로, 값의 출처인 훅에서 읽는 즉시 !!로 정규화해 모든 소비처가 항상 boolean만 받게 하는 것이 근본 수정이었다. 기존 사용자들의 localStorage에 남아 있는 "0"이나 "1" 같은 값도 이 정규화 한 번으로 함께 처리된다.
// before
const [collapsed, setCollapsed] = useLocalStorage(STORAGE_KEY, false)
// after
const [raw, setCollapsed] = useLocalStorage(STORAGE_KEY, false)
const collapsed = !!raw // 저장소=신뢰 경계, 읽는 즉시 boolean 강제
'리액트' 카테고리의 다른 글
| [리액트] 왜 서버컴포넌트에만 async 키워드를 붙일 수 있을까? (Next.js) (2) | 2025.07.17 |
|---|---|
| [리액트] JSX는 어떻게 자바스크립트로 변환될까? (feat. Babel, Virtual DOM) (0) | 2025.02.02 |