react 컴포넌트 분리하기

김상두·2023년 2월 24일
0

트러블슈팅

목록 보기
5/12
post-thumbnail

시작하며

react로 프로젝트를 개발하다보면 컴포넌트를 어떻게 분리해야할지에 대해 고민하게됩니다. 공식문서에서는 깊이 고민하지 말라고 하지만 프로젝트가 진행되다보면 이에 대한 고민이깊어지는것이 사실입니다. 이번 포스트에서는 컴포넌트 분리에 대한 고민을 소개합니다.

프로젝트 구성

제가 진행하는 프로젝트는 storybook을 이용해 모듈단위로 개발을 하면서 ui테스트도 진행하며, 비즈니스 로직은 jest 와 react-testing-libraray를 이용해서 테스트를 진행하고 있습니다.

테스트를 할때 가장 불편한 부분은, ui 컴포넌트에 비즈니스 로직이 결합되어있는 경우입니다. 가령, ui 컴포넌트에서 redux의 상태를 가져오게 되면, redux 상태를 mocking해야 초기 상태를 받아올수 있고, 수정도 쉽지않습니다.

container, presentational 패턴

hook이 존재하지도 않던 시절인 과거에 가장 많이 사용되었던 패턴으로 비즈니스 로직 (redux로직)을 보관하는 container 컴포넌트와 ui로직을 보관하는 presentational 컴포넌트로 구성하는 방식입니다

해당 방식을 사용하였을때의 이점은 표현(마크업)과 로직을 분리해 복잡도를 낮추고, view를 재사용할수 있다는데에 있습니다. 실제 코드를 보면 아래와 같습니다.

const TodoContainer = () => {
  const todoList = useSelector((state) => state.todo);
  const dispatch = useDispatch();
  const addListHandler = () => {
    dispatch(addList());
  };
  return <Todo list={todoList} addListHandler={addListHandler} />;
};

const Todo = ({list, addListHandler}) => {
  return (
    <main>
      {list.map((element) => (
        <div>{element}</div>
      ))}
      <button onClick={addListHandler}>아무할일 추가</button>
    </main>
  );
};

이 패턴을 창시한 Dan Abramov 가 2019년 수정한 글에서, 해당 패턴이 필요에 의해서 사용되는것은 괜찮지만, 무조건 사용될 필요는 전혀 없다고 하였습니다. 하지만 저는 ui를 분리한다는 아이디어가 좋았기에, 이 아이디어를 기반으로한 새로운 패턴으로 폴더구조를 가져가보고자 하였습니다.

ui, hook, container 패턴

먼저 view는 가능한 순수한 함수형태로 만들어져야한다고 생각했습니다. 비즈니스 로직이나 외부 상태가 결합될경우, mocking하기가 까다롭고 테스트도 쉽지 않기 때문입니다. 또한 custom hook을 이용해서 비즈니스로직을 관리해보고자 하였습니다. 두가지를 충족시키도록 구성한 패턴은 다음과같습니다.

ui컴포넌트는 view를 담당하는 파일로 입력된 상태에 따라서 출력을 보여주며
hook은 기존의 컨테이너 컴포넌트의 역할을 하는것으로 비즈니스 로직이 포함되며 container컴포넌트는 ui와 hook을 연결해 주는 파일로, 내부적으로 여러 custom hook을 사용해줄수도 있습니다.

이러한 패턴은 ui 로직과 비즈니스 로직이 모두 분리되어서 테스트가 편리하고 모두 재사용가능하다는 장점이 있지만, container라는 매개체가 들어가서 구조가 복잡해질수 있다는 단점이 있습니다. 따라서 테스트를 위해서가 아니라면 이러한 패턴을 굳이 사용할 필요는 없습니다.

const useTodo = () => {
  const todoList = useSelector((state) => state.todo);
  const dispatch = useDispatch();
  const addListHandler = () => {
    dispatch(addList());
  };
  return {todoList, addListHandler};
};

const TodoContainer = () => {
  const {todoList, addListHandler} = useTodo();
  return <Todo list={todoList} addListHandler={addListHandler} />;
};

const Todo = ({list, addListHandler}) => {
  return (
    <main>
      {list.map((element) => (
        <div>{element}</div>
      ))}
      <button onClick={addListHandler}>아무할일 추가</button>
    </main>
  );
};

ui 컴포넌트 분리하기

이제 남은것은 ui컴포넌트를 어느정도로 분리하는가 입니다. 사실 여기에는 정답이 없다고 생각합니다. 중요한건 재사용할수 있는 단위로 분리하는것과, 너무 많이 분리해서 복잡해지는 단위 사이에서 합리적인 지점을 찾는것입니다.

만약 댓글 기능을 구현하기 위해서 컴포넌트를 작성한다고 할때, 내부에 모든 ui로직을 작성할수도 있지만, 댓글 등록 버튼, 댓글 정렬 버튼, 댓글 입력 input을 분리해두면 이후 해당 부분을 수정할때 찾기 쉬워서 훨씬 수정하기 쉬울것이고 또한 재사용가능 측면에서도 장점이있습니다. 다만 너무 세세한부분 까지 분리하면 이를 결합하는데 복잡도가 늘어나기때문에, 적절한 분리 지점을 결정해야합니다.

마치며

폴더구조와 컴포넌트 분리에는 정답이 없는것 같습니다. 항상 그렇지만, 프로젝트의 성격에 맞는 폴더구조와 너무 복잡하지 않으면서 재사용성을 챙기는 그러한 분리 지점을 찾는것이 중요할것같습니다.

참고자료

https://ko.reactjs.org/docs/faq-structure.html
https://tecoble.techcourse.co.kr/post/2021-04-26-presentational-and-container/
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
https://jbee.io/react/testing-1-react-testing/

profile
프론트엔드 개발자 김상두입니다

0개의 댓글