컴포넌트 여행기(1) – 컴포넌트 분리하기

어떤 기준으로 컴포넌트를 분리할 것인가?

결론

  1. 관련 있는 컴포넌트들을 모아놓자.
  2. 공통 컴포넌트는 UI라이브러리를 만든다는 생각으로 작성하자(도메인 제거).
  3. 도메인이 겹치고 + 페이지간에 재사용되는 컴포넌트는 외부로 분리한다.
  4. 복잡한 추상화보다는 반복이 낫다.
  5. 적당히 나누자. 완벽히 나누게 되면 마음은 편하지만 코드를 찾아가는 경로가 길어지고 context를 잃게 된다.

관련 있는 컴포넌트들을 모아놓자

page 파일 안에 하위 컴포넌트 배치하기

// src/pages/detail-page/DetailPage.tsx

import Flex from '@shared/flex';

const DetailPage: React.FC = () => {
  const { isFetching, isError, isSuccess } = useQuery( ... );
  return (
    <Self>
      <Flex>
        <Sidebar>
          {isFetching && <Loading />}
          {isError && <Error />}
          {isSuccess && <MemberList />}
        </Sidebar>
        <Main>
          <Flex flexDirection="column" rowGap="40px">
            <StudyList />
            <TodoList />
          </Flex>
        </Main>
      </Flex>
    </Self>
  );
};

export default DetailPage;

const Self = () => { ... };
const Main = () => { ... };
const Sidebar = () => { ... };
const Loading = () => { ... };
const Error = () => { ... };
const MemberList = () => { ... };
const StudyList = () => { ... };
const TodoList = () => { ... };

이렇게 아래쪽에 배치하면 세가지 장점이 있습니다.

부모 컴포넌트로 빠르게 돌아올 수 있습니다

컴포넌트를 세세하게 파일별로 분리하면 VSC에서 부모컴포넌트 -> 하위 컴포넌트로 가는건 한번에 갈 수 있지만(ctrl + 우클릭), 다시 부모 컴포넌트로 오는건 개발자가 폴더 구조를 보고 찾아오거나 검색을 해야 하기 때문에 번거롭습니다.

아래쪽에 두게되면 해당 컴포넌트를 찾을때는 ctrl + 우클릭으로 한번에 가고, 돌아올때는 스크롤로 오면 되기 때문에 더 편합니다.

소속을 확실히 할 수 있습니다

세세하게 파일별로 나누면 이 컴포넌트가 어디에서 사용되는지 확실히 알기 어렵습니다. export를 하게 되니 컴포넌트 끼리 서로 import 할 수도 있고, 다른 페이지에서도 import 할 수 있게됩니다.

DetailPage.tsx 파일 아래쪽에 컴포넌트를 배치하면 해당 컴포넌트 들이 DetailPage에서만 사용됨이 보장됩니다.

const DetailPage: React.FC = () => { ... };

export default DetailPage;

// 이 컴포넌트 들은 DetailPage.tsx안에서만 사용됨이 보장됩니다.
const Self = () => { ... };
const Main = () => { ... };
const Sidebar = () => { ... };
const Loading = () => { ... };
const Error = () => { ... };
const MemberList = () => { ... };
const StudyList = () => { ... };
const TodoList = () => { ... };

폴더 구조가 단순해집니다

지금은 얼마 없지만 DetailPage안에 이런 저런 컴포넌트가 많아지고 이것들을 전부 다른 파일로 분리하면 폴더 구조가 길어집니다.

// 하위 컴포넌트가 많아지면 detail-page/components 하위에 폴더가 길어집니다

📦detail-page
 ┣ 📂components
 ┃ ┣ 📂error
 ┃ ┣ 📂loading
 ┃ ┣ 📂main
 ┃ ┣ 📂member-list
 ┃ ┣ 📂sidebar
 ┃ ┣ 📂study-list
 ┃ ┗ 📂todo-list
 ┣ 📜.DS_Store
 ┗ 📜DetailPage.tsx

반면, DetailPage.tsx아래에 두게 되면 폴더 구조가 심플해집니다.

문제점

응집성은 높아지지만, 해당 페이지의 컴포넌트가 많아지면 아래쪽이 너무 늘어나서 컴포넌트간의 구분이 어려워 지고 가독성이 떨어집니다. 네이밍 충돌도 빈번하게 일어날 수 있습니다.

const DetailPage: React.FC = () => { ... };

export default DetailPage;

const Self = () => { ... };
const Main = () => { ... };
const Sidebar = () => { ... };
const Loading = () = { ... };
const Error = () = { ... };

// MemberList 관련
const MemberList = () => { ... };
const MemberListItem = () => { ... };
const AddMemberButton = () => { ... };
const RemoveMemberButton = () => { ... };

// StudyList 관련
const StudyList = () => { ... };
const StudyListItem = () => { ... };
const AddStudyButton = () => { ... };
const RemoveStudyButton = () => { ... };

// TodoList 관련
const TodoList = () => { ... };
const TodoListItem = () => { ... };
const AddTodoItemButton = () => { ... };
const RemoveTodoItemButton = () => { ... };

그래서 우선 아래쪽에 구현을 하고, 때가 되면 파일로 분리하는 것이 좋다고 생각합니다.

대표적으로,

  • 컴포넌트가 복잡하고 무거울때
  • 스토리북에 표현해야할 필요가 있을때
  • 테스트가 필요한 컴포넌트일때

위 세가지 경우에 컴포넌트를 아래쪽이 아닌 같은 디렉토리의 독립적인 폴더에 두는것이 좋다고 생각합니다.

이후에 page 컴포넌트 아래쪽에는 다음과 같은 컴포넌트들이 남습니다.

  • 가벼운 컴포넌트들
    • Error
    • Loading
    • AddButton
    • PageTitle
  • Layout관련 컴포넌트들
    • Main
    • Sidebar

결과적으로 페이지 안에서 컴포넌트의 분리는 획일적인 방식 보다는 적당히 개발자가 덜 피곤한 방향으로 선택하는것이 좋다고 생각합니다.

네이밍 충돌

위처럼 아래쪽에 놓다보면 네이밍에 고민이 생길 때가 있습니다.

const Form = () => { ... };

export default Form;

const NameField = () => {
  return (
    ...
    <NameInput />
  );
};
const NameInput = () => {
  return <input />
};

const AgeField = () => {
  return (
    ...
    <AgeInput />
  );
};
const AgeInput = () => {
  return <input />
};

Input을 구분하기 위해서 prefix를 계속 붙여줘야 합니다. 이런 경우에는 class와 css props를 활용할 수 있습니다.

const NameField = () => {
  const style = css`
    ...
    .name-label {}
    .name-input {}
  `;
  return (
    <div css={style}>
      <label className='name-label' />
      <input className='name-input' />
    </div>
  );
};

name-label과 name-input이 외부에 노출(외부 css에 의해 영향을 받을 수 있음)되는 문제가 있지만, global css를 사용하지 않거나 BEM을 사용하면 해결할 수 있습니다.

page내부에서 분리된 컴포넌트

page내부에서 분리된 컴포넌트들도 마찬가지로 가급적 같은 파일의 아래쪽에 하위 컴포넌트들을 배치합니다.

// src/pages/detail-page/components/member-list/MemberList.tsx

const MemberList = () => { ... };
export default MemberList;

const Self = () => { ... };
const ListItem = () => { ... };
const AddButton = () => { ... };
const RemoveButton = () => { ... };

마찬가지로 너무 뚱뚱한 하위 컴포넌트는 다른 파일로 분리할 수 있습니다.

다만, 페이지와는 달리 하위 폴더로 만들기 보다는 이름을 잘 줘서 같은 계층에 폴더를 배치합니다.

📦detail-page
 ┣ 📂components
 ┃ ┣ 📂member-list
 ┃ ┣ 📂member-list-item // member-list 하위에 두지 않습니다!
 ┃ ┣ 📂...
 ┣ 📜.DS_Store
 ┗ 📜DetailPage.tsx

폴더 구조를 깊게 가져가면, 분류는 확실히 된다는 장점이 있지만 폴더 가독성이 떨어진다고 생각하기 때문입니다.

공통 컴포넌트 = UI Library

공통 컴포넌트는 ui library를 만든다는 관점으로 접근합니다.

공통 컴포넌트는 도메인에 관련된 내용이 없고 오로지 모양general한 기능을 가지고 있는 컴포넌트 입니다.

예를들어서,

📦components
 ┣ 📂@shared
 ┃ ┣ 📂avatar
 ┃ ┣ 📂button
 ┃ ┃ ┣ 📂box-button
 ┃ ┃ ┣ 📂icon-button
 ┃ ┃ ┣ 📂linked-button
 ┃ ┃ ┣ 📂text-button
 ┃ ┃ ┣ 📂toggle-button
 ┃ ┃ ┣ 📂unstyled-button
 ┃ ┃ ┗ 📜index.tsx
 ┃ ┣ 📂button-group
 ┃ ┣ 📂card
 ┃ ┣ 📂center
 ┃ ┣ 📂checkbox
 ┃ ┣ 📂chip
 ┃ ┣ 📂divider
 ┃ ┣ 📂drop-down-box
 ┃ ┣ 📂flex
 ┃ ┣ 📂form
 ┃ ┣ 📂icons
 ┃ ┃ ┣ 📂bookmark-icon
 ┃ ┃ ┣ 📂crown-icon
 ┃ ┃ ┣ 📂down-arrow-icon
 ┃ ┃ ┣ 📂folder-icon
 ┃ ┃ ┗ 📜index.tsx
 ┃ ┣ 📂image
 ┃ ┣ 📂infinite-scroll
 ┃ ┣ 📂input
 ┃ ┣ 📂label
 ┃ ┣ 📂letter-counter
 ┃ ┣ 📂list-item
 ┃ ┣ 📂markdown-render
 ┃ ┣ 📂meta-box
 ┃ ┣ 📂modal
 ┃ ┣ 📂multi-tag-select
 ┃ ┣ 📂page-title
 ┃ ┣ 📂page-wrapper
 ┃ ┣ 📂pagination
 ┃ ┣ 📂route-with-condition
 ┃ ┣ 📂section-title
 ┃ ┣ 📂select
 ┃ ┣ 📂textarea
 ┃ ┗ 📂user-info-item
 ┗ 📜.DS_Store

이런 식으로 도메인 정보가 없고 모양만 있는 컴포넌트를 @shared에 배치합니다.

페이지간 공통으로 사용되는 컴포넌트

페이지간에 공통으로 사용되는 컴포넌트는 어떨까요? 이 경우에는 2가지 케이스로 나뉩니다.

도메인이 같은 경우

도메인 정보가 들어가면서 여러 페이지 사이에서 중복되어 사용되는 컴포넌트인 경우에는 최상단 components 폴더에 배치합니다.

예를들어, study-chip 같은 경우 여러 페이지에서 사용 되고 그 의미도 동일하기 때문에 src/components 에 배치합니다.

📦components
 ┣ 📂@shared
 ┃ ┣ 📂...
 ┣ 📂study-chip

도메인이 다른데 모양이 같은 경우

이러한 경우에는 도메인을 제외한 모양만을 기준으로 공통 컴포넌트로 분리할 수 있는지 노력해보고, 어렵다면 그냥 중복을 허용합니다.

정리

  • 가능하면 같은 파일 내에 하위 컴포넌트를 배치합니다.
  • 때에 따라서 다른 파일로 컴포넌트를 분리합니다.
  • 공통 컴포넌트는 도메인 정보가 없이 모양만을 기준으로 만들고, src/components/@shared에 배치합니다.
  • 페이지간 공유되는 컴포넌트는 src/components에 배치합니다.
  • 핵심은 일관되고 완벽한 구조가 아닌, 개발자가 덜 피곤한 구조를 만드는것 입니다.

이상입니다. 읽어주셔서 감사합니다 😀

Leave a Reply

Your email address will not be published.