Component, Hook 언제 분리할까

react

나만의(?) 분리 기준을 살펴보자

컴포넌트: UI 유사성은 분리 기준이 아니다

생김새가 비슷하다고 같은 컴포넌트로 묶으면 안 된다.

// ❌ "버튼이니까 Button 컴포넌트로"
function Button({
  onClick,
  type,
  disabled,
  loading,
  icon,
  iconPosition,
  variant,
  size,
  fullWidth,
  ...
}) {
  // 온갖 조건 분기
}

<Button onClick={handleSubmit} variant="primary" loading={isLoading} />
<Button onClick={handleCancel} variant="ghost" />
<Button onClick={handleDelete} variant="danger" icon={<Trash />} />
<Button onClick={handleShare} icon={<Share />} iconPosition="right" />

과도한 추상화

버튼마다 기능과 맥락이 다른데, props를 optional로 계속 뚫으면 조건 분기가 늘어나고, 타입이 복잡해지고, 수정할 때 사이드이펙트를 예측하기 어려워진다.

그럼 언제 분리하나?

기능이 응집될 때. 하나의 책임, 하나의 목적을 가질 때 분리한다.

// ✅ 기능이 같은 것끼리 묶음
function PostCard({ post }) {
  return (
    <article>
      <h2>{post.title}</h2>
      <p>{post.description}</p>
      <TagList tags={post.tags} />
    </article>
  );
}

// PostList, RelatedPosts, SearchResults 등에서 동일한 목적으로 사용

PostCard는 "게시글을 카드 형태로 보여준다"는 하나의 목적만 가진다. UI가 비슷해서가 아니라, 하는 일이 같아서 분리한 것이다.


커스텀 훅: 훅이 여러 개라고 분리하는 게 아니다

// ❌ 독립적인 상태들을 그냥 묶어놓은 것 - 의미 없음
function usePostForm() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [tags, setTags] = useState("");
  const [loading, setLoading] = useState(false);

  return {
    title, setTitle,
    content, setContent,
    tags, setTags,
    loading, setLoading
  };
}

이건 useState 4개를 감싼 것뿐이다. 각 상태가 서로 독립적으로 동작하고, 사용처에서 결국 개별적으로 다뤄진다. 분리했다고 복잡도가 줄어들지 않는다.

그럼 언제 분리하나?

상태와 side effect가 강하게 결합되어 유기적으로 동작할 때.

// ✅ 상태 + 구독 + 액션 + 부수효과가 하나의 흐름으로 동작
function useAuth({ requireAuth = false } = {}) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const router = useRouter();

  useEffect(() => {
    supabase.auth.getUser().then(({ data: { user } }) => {
      setUser(user);
      setLoading(false);
      if (requireAuth && !user) router.push("/login");
    });

    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_, session) => setUser(session?.user ?? null)
    );

    return () => subscription.unsubscribe();
  }, [requireAuth]);

  const signOut = async () => {
    await supabase.auth.signOut();
    router.push("/");
  };

  return { user, loading, signOut };
}

이 훅은 "인증 상태 관리"라는 하나의 목적 아래, 상태 조회 → 구독 → 리다이렉트 → 로그아웃이 서로 엮여 동작한다.


정리

컴포넌트

  • UI가 비슷하다고 분리하지 않는다
  • 기능(책임)이 같을 때 분리한다

커스텀 훅

  • 훅이 여러 개라고 분리하지 않는다
  • 상태와 side effect가 하나의 목적으로 함께 동작할 때 분리

회고

참 어렵다. 같은 기준으로 프로젝트를 진행해본 적이 한번도 없는 것 같다. 분리하자니 너무 많은 폴더와 파일을 양산하는 것 같고, 분리 안하자니 일관성 있게 안짜는 것 같고...

line 수가 너무 길어진다는 이유로 분리할 때도 있고, 나중에 다른 곳에서 쓸 것 같다는 이유로 미리 추상화 할때도 있고...