본문으로 건너뛰기

"컴포넌트" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

모든 태그 보기

· 약 10분
최지훈

들어가며

글을 읽는 이유

  • FE와 BE 협업 프로젝트로 야놀자 클론 프로젝트를 한 경험이 있습니다.

  • 이때 가장 어려움을 겪었던 점은 공통 컴포넌트를 사용하는 페이지가 늘어나면서 공통 컴포넌트에 점점 많은 조건이 추가 되었고, 이로인해 더 이상 공통 컴포넌트라고 부를 수 없을 만큼 코드 복잡도가 높아졌던 점입니다.

  • 이에 프로젝트 종료 후 유지 보수에 어려움을 겪고있어 해당 글을 통해 컴포넌트를 분리하는 기준과 방법을 알아보고 싶습니다.

요약

  1. 컴포넌트는 웹 앱을 구성 하는 데 있어 가장 작은 단위이다.

  2. 컴포넌트를 나누는 기준

    A. 재사용성 컴포넌트

    • HTML 요소를 고려한 컴포넌트 분리
    • 중복을 고려한 컴포넌트 분리 (props, children 활용하기)

    B. 복잡성 컴포넌트

    • 컴포넌트가 여러 책임을 가질 때 분리
    • 컴포넌트에 비지니스 로직이 있을 때 분리

    C. 리렌더링 컴포넌트

    • 컴포넌트와 관련이 없는 상태가 있을 때 분리

주요 내용

컴포넌트에 대한 전반적인 내용

컴포넌트

  • 웹 앱을 구성 하는 데 있어 가장 작은 단위

컴포넌트를 만들 때 가장 많이 발생하는 실수 다섯 가지

  1. 복잡한 컴포넌트를 만든다.
  2. 하나의 컴포넌트에 여러 책임을 추가한다.
  3. 몇몇 동작하는 부분을 결합하여 컴포넌트를 만든다.
  4. 비지니스 로직을 컴포넌트에 추가한다.

언제 나눠야 할까?

컴포넌트를 만드는 기준, 즉 나누는 기준 중 가장 많이 선택되는 이유는 (A) 재사용성과 (B) 복잡성입니다.

컴포넌트를 나누는 기준

A. 재사용성 컴포넌트

  1. HTML 요소를 고려한 컴포넌트 분리

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

    위 코드의 컴포넌트를 분리한다고 했을때, li 태그로 묶은 ItemComponent 보다, 리스트 요소 말고 다른 곳에서도 사용할 수 있는 SomethingComponent를 하나의 컴포넌트로 묶는 것이 좋다.

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

    // 보다

    function SomethingComponent(...) {
    return (
    <>
    <h3>...</h3>
    <p>...</p>
    </>
    );
    }
  2. 중복을 고려한 컴포넌트 분리 (props, children 활용하기) 이렇게 둘 이상의 컴포넌트에서 사용할 재사용 가능한 컴포넌트를 만들 때 가장 큰 특징 중 하나는 조건문 입니다.

    완벽하게 같은 걸 사용하면 문제가 안 되지만 서로 다른 부분이 있다면 조건문이 들어가게 됩니다. 우리가 현실에서 마주하는 재사용 컴포넌트는 아래와 같이 점점 거대해지곤 합니다.

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

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

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

    Page1 컴포넌트와 Page2 컴포넌트는 Card 컴포넌트에 의존적입니다.

    만약 Page1과 Page2 뿐만 아니라 더 많은 컴포넌트가 Card 컴포넌트를 사용할수록 문제는 더 심각해집니다.

    이런 문제는 왜 발생했을까요? 그리고 어떤 문제를 일으킬까요?

    가장 먼저 컴포넌트가 반환하는 요소의 중복을 추출해서 재사용해야 한다는 접근 방법이 문제의 발단일 수 있습니다.

    추출한 컴포넌트 내부에 사용하는 방법에 따라 조건문이 추가된다는 건, 사용하는 컴포넌트들이 서로 다른 수정의 이유를 갖는 다는 걸 의미합니다. 즉, 중복 제거와 재사용의 대상이 아닙니다. 따라서 처음에 조건문이 들어갈 때부터 산불의 작은 불씨가 시작된 것이었습니다.

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

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

    이렇게 상태나 조건문 등의 결합이 사라진 것만으로도 Page1만의 특징인 summay는 Page1이 관리하고 Card는 이에 대해 신경 쓸 필요가 없습니다.

    특히나 이 방법은 props drilling을 피하거나 컴포넌트의 제어를 역전하는 등 좋은 점을 더 많이 갖고 있고 공식문서에서도 소개하는 만큼 반드시 숙지하고 있을 필요가 있습니다.

B. 복잡성 컴포넌트

  1. 컴포넌트가 여러 책임을 가질 때 분리

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

    이렇게 되면 기능 간에 결합이 강하게 발생해서 수정이 쉽지않습니다.

    그렇기 때문에 컴포넌트를 책임에 맞게 나눠서 단순화 해야 합니다.

    Page 컴포넌트가 탭, 검색, 페이징 그리고 이 정보들을 취합해 컨텐츠를 보여주는 등 모든 책임을 갖지 않도록 해야 합니다.

  2. 컴포넌트에 비지니스 로직이 있을 때 분리

    일반적으로 유저 인터페이스(UI)와 비지니스 로직은 변경의 속도, 즉 빈도가 다릅니다. 이때 컴포넌트에 비지니스 로직이 포함되어있다면 빈번한 UI 변경에 따라 자주 영향을 받을 수 있습니다.

    따라서 UI와 비지니스 로직을 적절하게 분리하는 건 소프트웨어를 오랫동안 유지보수 하는 데 있어서 아주 중요합니다.

C. 리렌더링 컴포넌트

  1. 컴포넌트와 관련이 없는 상태가 있을 때 분리

    하나의 컴포넌트 안에서 서로 영향을 주지 않는 상태가 여럿 있으면 불필요한 렌더링이 발생하는 문제입니다.

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

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

    이 코드에서 탭과 카드는 서로 영향을 주지 않습니다. 하지만 탭에 호버를 하면 카드들이 렌더링되고 카드에 호버를 하면 탭이 렌더링 됩니다.

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

    function Tab() {
    const [탭 호버 상태, set탭 호버 상태] = useState('none');

    return (
    <ul></ul>
    );
    }

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

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