리엑트 컴포넌트 설계와 SOLID 원칙

신병규·2023년 12월 25일
0
post-custom-banner

SOLID 원칙

단일 책임 원칙 - SRP (Single Responsibility Principle)

객체(클래스)는 단 하나의 책임만을 가져야 한다.

- 책임이란 기능 담당이라고 보면된다. 즉 하나의 객체는 하나의 기능을 담당하여 하나의 책임을 수행하는데 집중하도록 클래스를 따로 여러개 설계하는 원칙이다.
- 한 책임의 변경으로부터 다른 책임의 변경으로의 연쇄작용을 극복할 수 있게 된다.

⇒ SRP의 목적은 프로그램의 유지보수 성을 높이기 위한 설계 기법 

개방 폐쇄 원칙 - OCP (Open Closed Principle)

확장에 열려있어야 하며, 수정에는 닫혀있어야 한다.

- 기능 추가 요청이 오면 클래스 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정은 최소화 하도록 프로그램을 작성하는 설계 기법
⇒ 확장에 열려있다는 변경사항이 발생했을 때 유연하게 코드를 추가함으로써 애플리케이션 기능을 확장할 수 있음

⇒ 추상화 사용을 통한 관계 구축을 권장하는 것을 의미한다.
다형성과 확장을 가능케 하는 객체지향의 장점을 극대화하는 기본적 설계원칙이다.

리스코프 치환 원칙 - LSP (Liskov Substitutuin Principle)

서브 타입은 언제나 기반(부모) 타입으로 교체할 수 있어야 한다.

다형성의 원리를 이용하기 위한 원칙

- 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면, 업캐스팅된 상태에서 부모의 메서드를 사용해도 동작이 의도대로 흘러가야 하는 것을 의미
- 부모 메서드의 오버라이딩을 따져가며 해야함, 부모 클래스와 동일한 수준의 선행 조건을 기대하고 사용한 프로그램 코드에서 예상치 못한 문제를 일으킬 수 있기 때문

인터페이스 분리 원칙 - ISP (Interface Segregation Principle)

인터페이스를 각각 사용에 맞게 끔 잘게 분리해야한다는 설계 원칙

- 인터페이스의 단일 책임을 강조
- 인터페이스를 사용하는 클라이언트를 기준으로 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공하는 것이 목표
- 주의해야 할점은한번 인터페이스를 분리하여 구성해놓고 나중에 무언가 수정사항이 생겨서 또 인터페이스들을 분리하는 행위

⇒ 인터페이스는 제약 없이 다중 상속이 가능하기 때문에 분리할 수 있으면 분리하여 각 클래스 용도에 맞게 `implements` 하라는 원칙

의존 역전 원칙 - DIP(Dependency Inversion Principle)

DIP 원칙은 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그대상의 상위 요소(추상 클래스 or 인터페이스)로 참조하라는 원칙

- 구현 클래스에 의존하지 말고,인터페이스에 의존하라는 뜻
- 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는, 변화하기 어려운 것 거의 변화가 없는 것에 의존하라는 것

⇒ 클래스간의 결합도를 낮추는 것

리엑트에서 SOLID 적용

SRP

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>
  );
};

ActiveUsersList 함수는 fetch를 통해 데이터를 가져오고, filter를 통해 데이터를 가공하고, 컴포넌트를 렌더링 하는 등 많은 작업을 수행하고 있습니다.
이 함수는 너무 많은 기능을 수행하고 있어 문제 발생 시 유지보수가 어려워 질 수 있어 SRP 원칙에 따라 리펙토링하여 기능을 분리시켜 보겠습니다.

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 };
};

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

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

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

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

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

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

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>
  );
};

마지막 개선 사항으로 이 로직을 새로운 커스텀 hook으로 캡슐화할 수 있습니다.

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

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

OCP

소프트웨어 엔티티는 확장을 위해 열려야 하지만 수정을 위해 닫혀 있어야 한다

React 컴포넌트와 함수는 소프트웨어 엔티티이기 때문에 정의를 바꿀 필요가 없이 원래 형태대로 사용할 수 있습니다.

개방-폐쇄 원칙은 원본 소스 코드를 변경하지 않고 확장할 수 있는 방식으로 구성 요소를 구조화하도록 권고합니다.

예시 코드로 다른 페이지에서 공유된 Header 컴포넌트를 사용하는 애플리케이션에서 작업하고 있으며, 현재 있는 페이지에 따라 Header는 조금 다른 UI를 렌더링해야 합니다.

const Header = () => {
  const { pathname } = useRouter();

  return (
    <header>
      <Logo />
      <Actions>
        {pathname === "/dashboard" && (
          <Link to="/events/new">Create event</Link>
        )}
        {pathname === "/" && <Link to="/dashboard">Go to dashboard</Link>}
      </Actions>
    </header>
  );
};

const HomePage = () => (
  <>
    <Header />
    <OtherHomeStuff />
  </>
);

const DashboardPage = () => (
  <>
    <Header />
    <OtherDashboardStuff />
  </>
);

여기에서 현재 페이지에 따라 다른 페이지 컴포넌트를 위한 링크를 렌더링합니다. 더 많은 페이지를 추가하기 시작할 때 어떤 일이 일어날지 생각하면 이 구현이 나쁘다는 것을 쉽게 알 수 있습니다. 새 페이지가 생성될 때마다 Header 컴포넌트로 돌아가서 렌더링할 작업 링크를 알 수 있도록 구현을 조정해야 합니다. 이러한 접근 방식은 Header 컴포넌트는 취약해지고 사용되는 컨텍스트와 긴밀하게 결합되어, 개방-폐쇄 원칙에 위배됩니다.

const Header = ({ children }) => (
  <header>
    <Logo />
    <Actions>{children}</Actions>
  </header>
);

const HomePage = () => (
  <>
    <Header>
      <Link to="/dashboard">Go to dashboard</Link>
    </Header>
    <OtherHomeStuff />
  </>
);

const DashboardPage = () => (
  <>
    <Header>
      <Link to="/events/new">Create event</Link>
    </Header>
    <OtherDashboardStuff />
  </>
);

이 문제를 해결하기 위해 컴포넌트 합성(component composition)을 사용할 수 있습니다. Header 컴포넌트는 내부에서 무엇을 렌더링할지 신경 쓸 필요가 없으며 대신 children prop을 사용해서 Header를 사용할 컴포넌트에게 이 책임을 위임할 수 있습니다.

LSP

합성 (Composition) vs 상속 (Inheritance)

React는 강력한 합성 모델을 가지고 있으며, 상속 대신 합성을 사용하여 컴포넌트 간의 코드를 재사용하는 것이 좋습니다.

ISP

DIP

고수준 모듈이 저수준 모듈에 의존해서는 안된다.

SOLID 원칙을 지킨 컴포넌트

SOLID 원칙을 지키는 컴포넌트를 작성하는 것은, 각 컴포넌트가 명확한 역할을 수행하도록 함으로써 유지보수성과 코드의 가독성을 향상시키고 이렇게 하면 새로운 기능을 추가하거나 기존 기능을 수정하는 것이 훨씬 용이해집니다.

즉 컴포넌트 중심의 개발은 각 컴포넌트가 독립적이면서도 재사용 가능하게 설계되어야 하며, 이것이 가능하게 된다면 유연하고 가독성 높은 코드를 작성할 수 있습니다.

이러한 점을 고려해봤을 때, SOLID 원칙은 컴포넌트 중심의 개발이 어떻게 이루어져야 하는지를 설명하는 데 있어 상당히 유용한 가이드라인이라고 할 수 있습니다. 이 원칙을 따르면, 개발자는 각 컴포넌트가 어떤 역할을 수행해야 하는지, 어떻게 상호작용해야 하는지에 대한 명확한 이해를 바탕으로 코드를 작성할 수 있게 됩니다.

출처
https://medium.com/dailyjs/applying-solid-principles-in-react-14905d9c5377

post-custom-banner

0개의 댓글