team logo icon
article content thumbnail

컴포넌트 그거 어떻게 나누는데

서대원이 공유하는 컴포넌트 분리 기준과 방법입니다.

https://medium.com/@junep/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A5%BC-%EB%B6%84%EB%A6%AC%ED%95%98%EB%8A%94-%EA%B8%B0%EC%A4%80%EA%B3%BC-%EB%B0%A9%EB%B2%95-e7cf16bb157a

좋은 아티클을 제 생각과 함께 풀어서 다시 요약 정리했습니다!!!


🥷 컴포넌트를 분리에 대한 생각


컴포넌트가 뭔데

컴포넌트를 프론트엔드 라이브러리나 프레임워크에서 생각해본다면, 쉽게 말해서 웹 애플리케이션을 구성하는 가장 작은 단위라고 생각하면 된다. 이러한 단위를 분리하는 것에 대한 기준은 모두 제각각이다. 페이지를 만드는 것도 컴포넌트이고 아주 작은 요소를 만드는 것도 컴포넌트이다. 그렇기에 컴포넌트를 어느정도 까지 분리해야되는가에 대한 고민을 하게 된다.


컴포넌트 분리에 대한 유명한 기준에 대한 글이다 Presentational Components

https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

컴포넌트 왜 분리해?

어떤 프로젝트를 하던 프론트 개발자는 컴포넌트 분리에 대한 생각을 해야한다. 컴포넌트 하나에 모든 기능을 넣는다면 협업하는 개발자들이 코드를 알아보기도 힘들고, 재사용성을 기대하기도 어렵기 때문이다.. 이 처럼 컴포넌트 분리를 해야겠다는 생각은 개인적으로 프로젝트를 진행하며 자연스럽게 들었던것 같다

컴포넌트 분리 그거 어떻게 하는데

컴포넌트 분리? 다 좋다,, 근데 분리하기 위해서는 어떤 기준을 갖고 분리를 해야할까...? 그 기준을 어떻게 정할지에 대해서도 막막하다. 이 글에서는 컴포넌트를 분리하는 기준에 대해서 알아보도록 할것이다

🥷 컴포넌트를 분리 기준 제안


잠깐, 컴포넌트 만들 때 가장 많이 하는 실수 4가지

  1. 복잡한 컴포넌트를 만든다.

  2. 하나의 컴포넌트에 여러 책임을 추가한다.

  3. 몇몇 동작하는 부분을 결합하여 컴포넌트를 만든다.

  4. 비지니스 로직을 컴포넌트에 추가한다.

많이 발생하는 실수 4가지이다. 이러한 실수가 발생하게 되면 코드가 복잡해지고 재사용성은 저 멀리 가버릴것이다. 그럼 이런 실수를 어떤 기준을 갖고 방지해야할까

컴포넌트를 분리하는 기준 제안 3가지

컴포넌트를 분리하는 기준을 3가지 제안하겠다. 바로

  1. 재사용 가능하면 분리

  2. 복잡하면 분리

  3. 렌더링 퍼포먼스가 좋아진다면 분리

그럼 3가지 기준에 대해서 자세히 알아보자


🥷 컴포넌트 분리 기준 3가지


🎋 A. 재사용 가능하면 분리하자


1. 재사용이 가능하다는 것은 일반적이라는 것

아래 사진에서 탈 것이라는 것은 자동차, 말에 모두 포함되는 일반적인 개념이다. 우리는 재사용성을 높이기위해 이런 컴포넌트를 분리하여 일반적인 컴포넌트를 만들어야한다.


아래는 일반적인 컴포넌트의 예시이다.

function Button(...) {
  return <button>...</button>
}

function ButtonWithIcon(...) {
   return (
     <div>
       <i>...</i>
       <button>...</button>
     </div>
   );
}

Button 컴포넌트는 버튼을 사용하는 컴포넌트에 모두 포함되는 일반적인 컴포넌트이다. 하지만 ButtonWithIcon 컴포넌트는 비교적 일반적이지 않은 컴포넌트라고 할 수 있다.


그래서 결론이 뭐냐...

'다른 컴포넌트가 가져가서 사용할 수 있도록 보편적인 속성을 갖고 있는가?’를 고려해서 컴포넌트를 분리해야 한다는 것


2. HTML 요소 측면에서의 재사용성

아래 코드를 분리해보자... 어떻게 분리해야하지..?

 function ListComponent(...) {
  return (
    <ul>
      <li>
        <h3>...</h3>
        <p>...</p>
      </li>
      <li>
        <h3>...</h3>
        <p>...</p>
      </li>
    </ul>
  );
}


아 반복해서 쓰이는 요소를 따로 빼서 사용하면 되겠네!

아래 코드는 잘 분리한것 같아 보인다. 하지만 한가지 생각해봐야 할게 있다. 아래 처럼 분리 된 컴포넌트는 ul과 같은 리스트 요소에만 쓰일 수 있다는 것이다...

그럼 이 컴포넌트를 HTML 측면에서 재사용성을 높여서 고쳐보자!!!

function ItemComponent(...) {
  return (
    <li>
      <h3>...</h3>
      <p>...</p>
    </li>
  );
}


오오 이 컴포넌트는 반복되는 리스트 요소 뿐만 아니라, div 등 다른 요소안에서도 쓰일 수 있게, HTML 측면에서 재사용성이 높아졌다...

function SomethingComponent(...) {
  return (
    <>
      <h3>...</h3>
      <p>...</p>
    </>
  );
}


결론

이런식으로 HTML 측면에서도 재사용성을 높일 수 있어야한다.


3. 중복을 고려한 재사용성

이건 자연스럽게 드는 생각일 것이다. 중복된 요소가 있다면 따로 분리해서 컴포넌트로 만들어보자

아래 코드를 보자, 아래 코드에서 중복되는 요소를 따로 빼서 컴포넌트로 분리할 수 있다.

function Page1() {
  return (
    <ul>
      <li>
        <section>
          <h3>...</h3>
          <p>가격...</p>
          <p>요약...</p>
        </section>
      </li>
    </ul>
  );
}

function Page2() {
  return (
    <ul>
      <li>
        <section>
          <h3>...</h3>
          <p>가격...</p>
        </section>
      </li>
    </ul>
  );
}


분리한 코드는 아래와 같다

function Page1() {
  return (
    <ul>
      <li>
        <Card ... />
      </li>
    </ul>
  );
}

function Page2() {
  return (
    <ul>
      <li>
        <Card ... />
      </li>
    </ul>
  );
}
function Card(props) {
  return (
    <section>
      <h3>...</h3>
      <p>가격...</p>
      {props.showSummary && <p>요약...</p>}
    </section>
  );
}


이제 한결 재사용성을 높일 수 있었다.

그런데..

이렇게 둘 이상의 컴포넌트에서 사용할 재사용 가능한 컴포넌트를 만들 때 가장 큰 특징 중 하나는 조건문 이다. 완벽하게 같은 걸 사용하면 문제가 안 되지만 서로 다른 부분이 있다면 조건문이 들어가게 된다.


조건문을 많이 사용하게되고 재사용 컴포넌트가 복잡하게 되면 문제가 발생할 확률이 높다..

지금은 아주 간단한 컴포넌트라 문제가 잘 드러나지 않지만, 우리가 현실에서 마주하는 재사용 컴포넌트는 사용하는 컴포넌트가 늘어날 수록 점점 거대해지곤 한다.. 아래 처럼 말이다

// 추출 후 1달
function Card(props) {
  const [a, setA] = useState(props.a ? props.foo : props.bar);
  const condition1 = props.a && !props.b;
  
  return (
    <section>
      <h3>...</h3>
      <p>가격...</p>
      <div>
        <button>{a ? 'fooValue' : 'barBalue'}</button>
      </div>
      {props.showSummary && <p>요약...</p>}
      {condition1 && <div>...</div>}
    </section>
  );
}








이는 아래 사진과 같이 결합이 강력해졌기 때문이다. 강한 결합을 가진 컴포넌트는 변경이 어렵다. 결합력이 강력해졌기 때문에 재사용 컴포넌트의 조건문 변수 등이 많아졌을 것이고, 시간이 지나면서 사용하는 컴포넌트가 늘어 났을 것이다. 내부 로직이나 변수명을 고치게되면 이 재사용 컴포넌트를 사용하는 모든 컴포넌트에서 방대한 수정이 일어날 수 있기 때문에 함부로 변경이 어려워진다.




왜 이런 문제가 발생했을까? 중복에 대한 기준을 다시 생각해보자

만약 모습이 같은 두 코드가 같은 이유로 수정된다면 그 코드는 중복이다. 하지만 같은 모습의 코드라도 수정의 이유가 다르다면 두 코드는 중복이 아니라고 할 수 있다.


추출한 컴포넌트 내부에 사용하는 방법에 따라 조건문이 추가된다는 건, 사용하는 컴포넌트들이 서로 다른 수정의 이유를 갖는 다는 걸 의미한다. 즉, 중복 제거와 재사용의 대상이 아니다!

따라서 처음에 조건문이 들어갈 때부터 스노우볼이 굴려졌다고 할 수 있다


물론 완벽하게 위에서 언급한 중복의 조건을 반영하기란 어렵다. 조건문이 없는 컴포넌트를 만들기는 너무나 어려운 일이기 때문이다. 그러므로 조건문을 추가할 때 컴포넌트의 관계에 어떤 영향을 주는지 이해하는 것이 중요하다.


그럼 이런 문제를 어떻게 해결할까

이 문제들을 해결하는 방법 중 하나는 재사용하려는 컴포넌트에는 정말 공통적인 것들만 남겨두고 사용하는 컴포넌트의 고유한 것은 속성으로 전달하는 것이다.


function Page1() {
  return (
    <ul>
      <li>
        <Card
          summary={<p>요약...</p>}
        />
      </li>
    </ul>
  );
}

function Page2() {
  return (
    <ul>
      <li>
        <Card ... />
      </li>
    </ul>
  );
}

function Card(props) {
  return (
    <section>
      <h3>...</h3>
      <p>가격...</p>
      {props.summary} // && 조건문이 제거되고 prop으로(속성) 추가적인 컴포넌트 요소 관리
    </section>
  );
}


이렇게 하면 Page1만의 특징인 summay는 Page1이 관리하고 Card는 이에 대해 신경 쓸 필요가 없게 된다! 이 말은 Page1에서 summary로 전달하는 요소를 수정해도 Page2 컴포넌트를 살펴볼 필요가 없다는 것을 의미한다!!


위와 같이 속성으로 컴포넌트를 전달하는것에 대한 중요성이 잘 정리된 글이 있다

https://www.developerway.com/posts/react-component-as-prop-the-right-way


결론

중복되는 요소를 분리하는것 좋다. 하지만 중복에 대해서 아래와 같이 생각해보자.

만약 모습이 같은 두 코드가 같은 이유로 수정된다면 그 코드는 중복이다. 하지만 같은 모습의 코드라도 수정의 이유가 다르다면 두 코드는 중복이 아니라고 할 수 있다.

또한

조건문을 줄이고 prop으로만 추가적인 요소를 관리한다면 해당 prop을 사용하지 않는 요소에서는 이를 신경쓸 필요가 없어지며 재사용성이 증가한다


🥷 B. 복잡하면 분리


1. 컴포넌트가 여러 책임을 갖는 경우

상식적으로 생각해도 너무 복잡하다. 하나의 컴포넌트가 너무 많은 책임을 갖는다면.. 기능간에 결합이 강하게 발생해서 수정이 어렵다. 거대한 프로젝트에서는 거의 있을 수 없는 일이다. 아래 예시를 보자

function Page(props) {
  // 선택한 탭을 변경하면 보여주는 내용을 변경합니다.
  // 페이징을 다룹니다.
  // 단어를 검색을 합니다.
  // 검색 조건 토글을 다룹니다.
  // 등등
}


그렇기 때문에 이를 아래와 같이 분리해야한다

각 컴포넌트는 각자의 UI에 대한 책임(B)을 갖고 있다. 그리고 컨텐츠 컴포넌트와의 소통은 A라는 경로를 통해서만 이루어진다. 또한 다른 컴포넌트와의 직접적인 상호작용(C)는 제거할것이다.


결론

이런식으로 많은 책임을 가져 복잡한 컴포넌트를 분리해야한다



2. 컴포넌트에 비즈니스 로직이 있는 경우

일반적으로 유저 인터페이스(UI)와 비지니스 로직은 변경의 속도, 즉 빈도가 다르다.

예를 들어,

UI: 상품 카드에 환불이 가능한지 표시하는 방법은 많고 수시로 바꿀 수 있으며, 환불 문구의 색상또한 수시로 바꿀수 있다.

Business logic: 환불 가능한 조건을 변경하는 건 사업 초기에 한 번 정해지고 바뀌지 않을 수도 있다



비지니스 로직은 현실 세계의 비지니스 규칙이기 때문에 영향을 자주 받고 빈번하게 변경이 되면 문제가 될 수 있다.

그래서 하나의 컴포넌트안에 비지니스 로직이 포함되어있다면! 빈번한 UI 변경에 따라 자주 영향을 받고 문제가 될 수 있다. ( 되도록 비즈니스 로직 부분을 변경하지 않는게 좋다는 말이다.)


하나의 컴포넌트가 자식 컴포넌트 A,B를 갖고 있고, 그 안에서 비즈니스 로직을 포함하고 있다고 가정하자.

컴포넌트 A,B 는 공통적으로 환불 가능 여부에 따라 true 또는 false가 필요하고, 컴포넌트 B에서는 추가적으로 상품의 타입도 같이 필요해졌다고 생각해보자. 이에 따라서 아래와 같이 비지니스 로직의 코드를 변경해야한다(Bad).

function isRefundable(product) {
  // Business Logic
  return true or false;
}

// 이 함수를 아래와 같이 변경합니다.

function isRefundable(product) {
  // Business Logic
  return true or false;
}

function getRefundableWithType(product) {
  // Business Logic
  return {
    type: product type,
    isRefundable: true or false,
  };
}

// 이 함수를 또 아래와 같이 변경합니다.

function refundable(product) {
  // Business Logic
  return true or false;
}

function isRefundable(product) {
  return refundable(product);;
}

function getRefundableWithType(product) {
  return {
    type: refundable(product);,
    isRefundable: true or false,
  };
}

// 등등..


결론

비즈니스 로직은 자주 변경되면 문제가 생길 가능성이 크다.

그런데 비즈니스 로직을 컴포넌트에 포함시키면 UI의 변경에 따라 영향 받아 자주 변경 될 수 있으니 비즈니스 로직과 UI를 적절하게 분리하자.


🎋 C. 렌더링 퍼포먼스가 좋아지면 분리

재사용과 복잡성 이외에 컴포넌트를 분리하면 좋은 기준 중 하나는 렌더링 퍼포먼스이다.

주로 하나의 컴포넌트 안에서 서로 영향을 주지 않는 상태가 여럿 있을 때 발생하는 문제이다


아래의 예시를 보자.

아래 코드에서 탭과 카드는 서로 영향을 주지 않는다. 하지만 탭에 호버를 하면 카드들이 렌더링되고 카드에 호버를 하면 탭이 렌더링 된다. 즉 서로 영향을 주지 않는 상태 때문에 불필요하게 리렌더링이 되고 있는 것이다. 이를 분리해보자

function Page1() {
  const [카드 호버 상태, set카드 호버 상태] = useState(false);
  const [탭 호버 상태, set탭 호버 상태] = useState('none');

  return (
    ...
    <ul>탭</ul>
    ...
    <ul>카드</ul>
    ...
  );
}








아래의 코드 처럼 적절하게 분리해보자. 자 이제 각자의 상태가 변경 될 때만 컴포넌트 리렌더링이 일어나게 되어, 렌더링 퍼포먼스가 향상되었다.

function Page1() {
  return (
    ...
    <Tab></Tab>
    ...
    <ul>
      ...
      <li><Card></Card><li>
      ...
    </ul>
    ...
  );
}

function Tab() {
  const [탭 호버 상태, set탭 호버 상태] = useState('none');
  
  return (
    <ul>탭</ul>
  );
}

function Card() {
  const [카드 호버 상태, set카드 호버 상태] = useState(false);

  return (
    <section>...</section>
  );
}


결론

렌더링 퍼포먼스가 좋아지면 분리하자


마무리


지금까지 컴포넌트를 분리하는 특정 기준에 대해서 자세하게 알아봤다. 이러한 기준들이 본인 만의 컴포넌트를 분리하는 기준을 세우는데에 도움이 됐으면 좋겠다. 파이팅




최신 아티클
lighthouse에 대해
문성희
|
2024.05.13
lighthouse에 대해
lighthouse에 대해
prettier, eslint, styleLint에 대해
이진
|
2024.05.10
prettier, eslint, styleLint에 대해
4주차 공유과제
Article Thumbnail
박채연
|
2024.05.10
Prettier, ESLint, StyleLint
prettier, eslint, stylelint