view와 강결합된 UI 상태는 컴포넌트에 남기기
렌더링 형태와 1:1로 대응하는 로컬 UI 상태(isOpen, isFocused 등)는 훅으로 분리하지 않고 컴포넌트 본문에 남깁니다. 빼는 순간 아무것도 숨기지 못하는 불필요한 추상화가 생깁니다.
좋지 않은 예시
tsx
// hook
export const useDropdown = () => {
const [isOpen, setIsOpen] = useState(false);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
const toggle = () => setIsOpen((prev) => !prev);
return { isOpen, open, close, toggle };
};
// component
export const SortDropdown = () => {
const { isOpen, toggle } = useDropdown();
return (
<div>
<button onClick={toggle}>정렬</button>
{isOpen && <SortOptions />}
</div>
);
};좋은 예시
tsx
export const SortDropdown = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen((prev) => !prev)}>정렬</button>
{isOpen && <SortOptions />}
</div>
);
};왜
isOpen은 이 컴포넌트가 펼쳐졌는지를 그대로 나타내는, 렌더링과 1:1로 대응하는 상태입니다. 훅으로 빼면 컴포넌트의 렌더링과 직결된 상태를 읽으려 다른 파일로 건너뛰어야 하고, useDropdown이라는 이름은 useState 한 줄이 이미 말해주는 것 이상을 숨기지 못합니다. 이런 상태는 같은 추상화 레벨, 즉 컴포넌트 본문에 두는 것이 맞습니다.
주의사항
이 원칙은 모달, 툴팁, 드롭다운처럼 렌더링 형태에 직결되는 presentational 상태에만 적용됩니다. 폼 검증, 서버 데이터, 비동기 처리처럼 화면 밖에서도 의미를 갖는 상태는 오히려 훅으로 분리하는 것이 맞습니다.
