본문으로 건너뛰기

"props" 태그로 연결된 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>
    );
    }