[번역] 리액트에서의 단일 책임 원칙: 컴포넌트 집중의 기술

eunbinn·2025년 3월 9일
56

FrontEnd 번역

목록 보기
41/41
post-thumbnail

출처: https://cekrem.github.io/posts/single-responsibility-principle-in-react/

소개

우리는 지금까지 의존성 역전 원칙, 인터페이스 분리 원칙, 리스코프 치환 원칙, 그리고 개방-폐쇄 원칙에 대해 다뤄보았습니다. 이제 SOLID의 기초가 되는 단일 책임 원칙(SRP)을 살펴볼 차례입니다.

다시 한번, 고전적인 저서 'Clean Architecture'를 통해 좋은 소프트웨어 아키텍처의 중요성을 일깨워주신 Uncle Bob에게 감사드립니다! 이 시리즈는 그의 책에서 주된 영감을 얻었습니다.

단일 책임 원칙이란 하나의 클래스는 오직 하나의 이유로만 변경되어야 한다는 원칙입니다.

다중 책임의 문제점

다음 코드는 흔히 발견되는 안티패턴 입니다.

// 이렇게 작성하지 마세요
const UserProfile = () => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetchUser();
  }, []);

  const fetchUser = async () => {
    try {
      const response = await fetch("/api/user");
      const data = await response.json();
      setUser(data);
    } catch (e) {
      setError(e as Error);
    } finally {
      setLoading(false);
    }
  };

  const handleUpdateProfile = async (data: Partial<User>) => {
    try {
      await fetch("/api/user", {
        method: "PUT",
        body: JSON.stringify(data),
      });
      fetchUser(); // Refresh data
    } catch (e) {
      setError(e as Error);
    }
  };

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <form onSubmit={/* form logic */}>{/* Complex form fields */}</form>
      <UserStats userId={user.id} />
      <UserPosts userId={user.id} />
    </div>
  );
};

이 컴포넌트는 다음과 같은 여러 책임을 가지고 있어 단일 책임 원칙에 위배됩니다.

  1. 데이터 페칭
  2. 에러 핸들링
  3. 로딩 상태 관리
  4. 폼 핸들링
  5. 레이아웃과 프레젠테이션

더 나은 방법: 관심사를 분리하기

이를 각각의 역할에 집중화 된 컴포넌트들로 분리해보겠습니다.

// 데이터 페칭 훅
const useUser = (userId: string) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetchUser();
  }, [userId]);

  const fetchUser = async () => {
    try {
      const response = await fetch(`/api/user/${userId}`);
      const data = await response.json();
      setUser(data);
    } catch (e) {
      setError(e as Error);
    } finally {
      setLoading(false);
    }
  };

  return { user, loading, error, refetch: fetchUser };
};

// 프레젠테이션 컴포넌트
const UserProfileView = ({
  user,
  onUpdate,
}: {
  user: User;
  onUpdate: (data: Partial<User>) => void;
}) => (
  <div>
    <h1>{user.name}</h1>
    <UserProfileForm user={user} onSubmit={onUpdate} />
    <UserStats userId={user.id} />
    <UserPosts userId={user.id} />
  </div>
);

// 컨테이너 컴포넌트
const UserProfileContainer = ({ userId }: { userId: string }) => {
  const { user, loading, error, refetch } = useUser(userId);

  const handleUpdate = async (data: Partial<User>) => {
    try {
      await fetch(`/api/user/${userId}`, {
        method: "PUT",
        body: JSON.stringify(data),
      });
      refetch();
    } catch (e) {
      // 에러 핸들링
    }
  };

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <NotFound message="User not found" />;

  return <UserProfileView user={user} onUpdate={handleUpdate} />;
};

핵심 요약

  1. 데이터와 UI를 분리하기 - 데이터는 훅을 사용하고, UI는 컴포넌트를 사용합니다
  2. 집중화 된 컴포넌트 만들기 - 각 컴포넌트는 한 가지 일만 잘 수행해야 합니다
  3. 단순한 부분들로 복잡한 기능을 구현하기 위해 합성(composition)을 활용합니다
  4. 재사용 가능한 로직은 커스텀 훅으로 추출합니다
  5. 계층적으로 생각하기 - 데이터, 비즈니스 로직, 프레젠테이션

결론

각 컴포넌트가 단 하나의 명확한 책임을 갖도록 설계하면, 전체 애플리케이션이 더욱 유지보수하기 쉽고, 테스트하기 쉬우며, 유연해집니다.

Uncle Bob이 클린 아키텍처에서 강조했듯이, '변경해야 하는 이유가 하나여야 한다'는 것입니다. 다음의 미묘한 차이를 이해하는 것이 중요합니다.

  • 하나의 컴포넌트가 여러가지 연관된 일을 하지만, 만약 그것들이 모두 같은 이유로 변경된다면(예: 사용자 프로필 UI 업데이트), 이는 아마도 함께 있어야 할 것입니다
  • 반대로, 겉보기에 단순해 보이는 두 가지 작업이라도 서로 다른 이유로 변경이 필요하다면(예: 사용자 환경설정 vs 인증 로직), 분리될 필요가 있습니다

프로 팁: 컴포넌트가 하는 일을 설명할 때 '그리고'라는 단어를 사용하게 된다면, 이는 아마도 단일 책임 원칙을 위반하고 있을 수 있습니다. 분리하세요! 하지만 동시에 각 부분이 왜 변경될 수 있는지, 그리고 누가 그러한 변경을 요청할 수 있는지도 함께 고려해봐야 합니다.

참고해주세요

소프트웨어 아키텍처에 대한 포괄적인 가이드를 찾고 계신다면, 이 글은 그런 글이 아닙니다. 최근 제가 작성한 소프트웨어 아키텍처에 관한 게시물들의 목적은 제가 이전에 너무 쉽게 무시했거나 적용하기를 게을리했던 몇 가지 원칙들을 실용적인 방식으로 탐구해보는 것이었습니다. 저는 이러한 개념들을 완벽히 마스터했다고 주장하는 것도 아니고, 이러한 원칙들을 모든 상황에 엄격하게 적용해야 한다고 제안하는 것도 아닙니다. 심지어 제가 든 간단한 예시들이 이러한 원칙들을 구현하거나 설명하는 최선의 방법이라고 주장하는 것도 아닙니다. 오히려, 저는 고전적인 소프트웨어 공학 원칙들과 현대적인 개발 방식을 연결하려는 제 시도를 기록하고 있는 것입니다. 사실, 저는 아직 최종적으로 얼마나 '클린 아키텍처'에 가깝게 갈 것인지 혹은 얼마나 실용적으로 접근할 것인지를 결정하지 못했습니다. 하지만 지금은 이러한 학습과 탐구 과정을 (대체로) 즐기고 있습니다. 레딧에서 저나 Uncle Bob, 혹은 다른 누군가를 비난하기 전에 이 점을 염두에 둬주시면 감사하겠습니다 😅

2개의 댓글

comment-user-thumbnail
2025년 3월 11일

각각의 컴포넌트를 따로 파일로 분리할 필요 없이 하나의 파일 내에서 SRP에 따라 여러 컴포넌트를 정의해도 괜찮은건가요?

답글 달기
comment-user-thumbnail
5일 전

잘봤습니다

답글 달기

관련 채용 정보