Front-end/React

[SOLID] React에 단일책임원칙 적용하기

이안92 2024. 2. 11. 18:02
반응형

 ✍🏻 단일 책임 원칙 (SRP)

 

원래 정의는 "모든 클래스는 단 하나의 책임만 가져야 한다"라고 명시되어 있습니다. 이 원칙은 해석하기 가장 쉽습니다. "모든 함수/모듈/컴포넌트는 정확히 한 가지 작업을 수행해야 한다"라는 정의라고 해석할 수 있기 때문입니다.

5가지 원칙 중 SRP는 가장 따르기 쉽지만 코드 품질이 크게 향상하기 때문에 가장 영향력 있는 원칙이기도 합니다. 컴포넌트가 한 가지 작업을 수행하도록 하기 위해 다음을 수행할 수 있습니다.

  • 너무 많은 작업을 수행하는 큰 컴포넌트를 더 작은 컴포넌트로 나눔
  • 주요 컴포넌트 기능과 관련 없는 코드를 별도의 유틸리티 함수로 추출
  • 관련 있는 기능들을 커스텀 hook으로 캡슐화
👨🏻‍💻 React에 적용해보자!

 

이제 이 원칙을 적용하는 방법을 살펴보겠습니다. 먼저 활성 사용자 목록을 표시하는 다음 예제 컴포넌트를 고려해서 시작해보겠습니다.

const ActiveUsersList = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const loadUsers = async () => {
      const response = await fetch("/some-api");
      const data = await response.json();
      setUsers(data);
    };

    loadUsers();
  }, []);

  const weekAgo = new Date();
  weekAgo.setDate(weekAgo.getDate() - 7);

  return (
    <ul>
      {users
        .filter((user) => !user.isBanned && user.lastActivityAt >= weekAgo)
        .map((user) => (
          <li key={user.id}>
            <img src={user.avatarUrl} />
            <p>{user.fullName}</p>
            <small>{user.role}</small>
          </li>
        ))}
    </ul>
  );
};

 

이 컴포넌트는 현재 비교적 짧지만 데이터를 가져오고, 필터링하고, 컴포넌트 자체와 목록의 각 항목을 렌더링하는 등 이미 많은 작업을 수행하고 있습니다. 어떻게 나눌 수 있는지 봅시다.

우선 서로 연관된 useState와 useEffect가 있을 때는 언제든지 커스텀 hook으로 추출할 수 있습니다.

 

const useUsers = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const loadUsers = async () => {
      const response = await fetch("/some-api");
      const data = await response.json();
      setUsers(data);
    };

    loadUsers();
  }, []);

  return { users };
};

const ActiveUsersList = () => {
  const { users } = useUsers();

  const weekAgo = new Date();
  weekAgo.setDate(weekAgo.getDate() - 7);

  return (
    <ul>
      {users
        .filter((user) => !user.isBanned && user.lastActivityAt >= weekAgo)
        .map((user) => (
          <li key={user.id}>
            <img src={user.avatarUrl} />
            <p>{user.fullName}</p>
            <small>{user.role}</small>
          </li>
        ))}
    </ul>
  );
};

 

이제 useUsers hook은 오직 API에서 사용자를 가져오는 것 하나에만 관련되어 있습니다. 또한 메인 컴포넌트 코드가 더 짧아졌을 뿐만 아니라 용도를 파악해야 하는 구조적인 hook을 이름에서 용도를 바로 알 수 있는 도메인 hook으로 대체했기 때문에 보다 읽기 쉬워졌습니다.

다음으로 컴포넌트가 렌더링하는 JSX를 살펴보겠습니다. 객체 배열을 순회하며 매핑하는 경우 배열의 각 항목에 대해 생성하는 JSX의 복잡성에 주의를 기울여야 합니다. 이벤트 핸들러가 연결되지 않은 한 줄짜리 마크업인 경우 인라인으로 유지하는 것이 좋지만 더 복잡한 마크업의 경우 별도의 컴포넌트로 추출하는 것이 좋습니다.

 

const UserItem = ({ user }) => {
  return (
    <li>
      <img src={user.avatarUrl} />
      <p>{user.fullName}</p>
      <small>{user.role}</small>
    </li>
  );
};

const ActiveUsersList = () => {
  const { users } = useUsers();

  const weekAgo = new Date();
  weekAgo.setDate(weekAgo.getDate() - 7);

  return (
    <ul>
      {users
        .filter((user) => !user.isBanned && user.lastActivityAt >= weekAgo)
        .map((user) => (
          <UserItem key={user.id} user={user} />
        ))}
    </ul>
  );
};

 

이전 변경과 마찬가지로 사용자 항목을 렌더링하는 로직을 별도의 컴포넌트로 추출하여 메인 컴포넌트를 더 작고 읽기 쉽게 만들었습니다.

마지막으로 API로부터 얻은 전체 사용자 목록에서 비활성 사용자를 필터링하는 로직이 있습니다. 이 로직은 비교적 독립되어 있고 애플리케이션의 다른 부분에서 재사용될 수 있으므로 유틸리티 함수로 쉽게 추출할 수 있습니다.

 

const getOnlyActive = (users) => {
  const weekAgo = new Date();
  weekAgo.setDate(weekAgo.getDate() - 7);

  return users.filter(
    (user) => !user.isBanned && user.lastActivityAt >= weekAgo
  );
};

const ActiveUsersList = () => {
  const { users } = useUsers();

  return (
    <ul>
      {getOnlyActive(users).map((user) => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
};

 

이 시점에서 메인 컴포넌트는 더 이상 나누지 않아도 될 만큼 짧고 간단합니다. 그러나 조금 더 자세히 살펴보면 여전히 해야 할 것보다 더 많은 일을 하고 있음을 알 수 있습니다. 현재 컴포넌트는 데이터를 페칭한 다음 필터링을 적용하고 있지만 추가 조작 없이 데이터를 가져와 렌더링하기만 하는 것이 이상적입니다. 따라서 마지막 개선 사항으로 이 로직을 새로운 커스텀 hook으로 캡슐화할 수 있습니다.

 

const useActiveUsers = () => {
  const { users } = useUsers();

  const activeUsers = useMemo(() => {
    return getOnlyActive(users);
  }, [users]);

  return { activeUsers };
};

const ActiveUsersList = () => {
  const { activeUsers } = useActiveUsers();

  return (
    <ul>
      {activeUsers.map((user) => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
};

 

여기서는 페칭하고 필터링하는 로직을 처리하기 위해 useActiveUsers hook을 만들어 메인 컴포넌트에는 hook에서 가져온 데이터를 렌더링하는 최소한의 작업만 남겨둡니다. (좋은 성능을 위해 필터링된 데이터를 메모이제이션했습니다)

 

이제 "한 가지"를 어떻게 해석하는지에 따라 컴포넌트가 여전히 먼저 데이터를 얻은 다음 렌더링하기 때문에 "한 가지"가 아니라고 주장할 수 있습니다. 한 컴포넌트에서 hook을 호출한 다음 그 결과를 다른 컴포넌트에 props로 전달해서 더 나눌 수 있지만 실제 애플리케이션에서 이것이 실제로 이로운 경우는 거의 없었습니다. 때문에 정의를 "컴포넌트가 얻은 렌더링 데이터"를 "한 가지"로 너그럽게 받아들이도록 합시다.

 

요약하자면, 단일 책임 원칙에 따라 우리는 큰 모놀리식 코드 덩어리를 효과적으로 가져와 더 모듈화합니다. ✨✨✨모듈화하면 코드를 파악하기 쉬워지고, 의도치 않은 중복 코드를 작성할 가능성이 줄어듭니다. 또한 작은 모듈은 테스트 및 수정하기 더 쉽기 때문에 결과적으로 코드를 보다 쉽게 유지 관리할 수 있어 좋습니다. ✨✨✨

 

여기에서 본 것은 인위적인 예이며, 여러분의 컴포넌트는 서로 다른 가동부들 사이에 의존성이 훨씬 더 얽혀 있다는 것을 알 수 있습니다. 대부분의 경우 이는 적절하지 못한 추상화, 다재다능한 전역 컴포넌트 생성, 데이터의 잘못된 스코프 설정 등 잘못된 설계를 선택했기 때문입니다. 그리고 이는 광범위한 리팩토링으로 해결할 수 있습니다.

 

 👀 출처 링크

Applying SOLID principles in React

[번역] React에 SOLID 원칙 적용하기

 

반응형