[React] 컴포넌트를 얼마나 쪼개야 좋을까? 🤔

Joo·2024년 3월 12일

React

목록 보기
6/11
post-thumbnail

++ 리액트를 더 잘 쓰기 위한 정리 시리즈

리액트의 큰 장점 중 하나는 컴포넌트의 재사용성이다. 같은 내용의 코드를 반복하지 않고도 커스텀 컴포넌트를 내장된 요소들처럼 사용할 수 있다.
하지만 이런 컴포넌트를 얼마나 쪼개서 써야 좋을까?

컴포넌트를 쪼개는 이유 1

만약 App 컴포넌트 안에 여러 컴포넌트들을 다 둔다면 전체 코드 길이도 길어지고, 가독성도 떨어진다. 더 중요한 것은 예상치 못한 side effect가 발생할 수 있다.

예를 들어, 아래 코드처럼 topic을 useState 훅으로 관리할 경우 선택한 탭이 바뀌면 App 컴포넌트 전체가 리렌더링 된다.
변경될 필요가 없는 부분까지 영향을 주는 것이다. 이런 상황을 방지하기 위해서라도 탭과 관련된 부분을 쪼개보자!

import { useState } from 'react';

function App() {

  return (
    <div>
      <Header />
      <main>
        <section id="core-concepts">
          <h2>Core Concepts</h2>
          <ul>
            {CORE_CONCEPTS.map((conceptItem) => (
              <CoreConcept key={conceptItem.title} {...conceptItem} />
            ))}
          </ul>
        </section>
        <section id="examples">
          <h2>Examples</h2>
          <menu>
            <TabButton
              isSelected={selectedTopic === 'components'}
              onSelect={() => handleSelect('components')}
            >
              Components
            </TabButton>
            <TabButton
              isSelected={selectedTopic === 'jsx'}
              onSelect={() => handleSelect('jsx')}
            >
              JSX
            </TabButton>
            ... 생략
          </menu>
          {tabContent}
        </section>
      </main>
    </div>
  );
}

export default App;

App 컴포넌트에서 설명 부분인 CoreConcepts와 탭과 관련된 내용인 Examples라는 컴포넌트로 분리했다. 그 결과 App 컴포넌트는 엄청 간단해졌고, 더이상 탭을 변경해도 앱 전체에 영향을 주지 않는다!

App.jsx

import Header from "./components/Header/Header.jsx";
import Examples from "./components/Examples.jsx";
import CoreConcepts from "./components/CoreConcepts.jsx";

function App() {
  return (
    <div>
      <Header />
      <main>
        <CoreConcepts />
        <Examples />
      </main>
    </div>
  );
}

export default App;

CoreConcepts.jsx

import { CORE_CONCEPTS } from "../data.js";
import CoreConcept from "./CoreConcept.jsx";

export default function CoreConcepts() {
  return (
    <section id="core-concepts">
      <h2>Core Concepts</h2>
      <ul>
        {CORE_CONCEPTS.map((conceptItem) => (
          <CoreConcept key={conceptItem.title} {...conceptItem} />
        ))}
      </ul>
    </section>
  );
}

Examples.jsx

import { useState } from "react";
import TabButton from "./TabButton.jsx";
import { EXAMPLES } from "../data.js";

export default function Examples() {
  const [selectedTopic, setSelectedTopic] = useState();

  function handleSelect(selectedButton) {
    // selectedButton => 'components', 'jsx', 'props', 'state'
    setSelectedTopic(selectedButton);
    // console.log(selectedTopic);
  }

  console.log("APP COMPONENT EXECUTING");

  let tabContent = <p>Please select a topic.</p>;

  if (selectedTopic) {
    tabContent = (
      <div id="tab-content">
        <h3>{EXAMPLES[selectedTopic].title}</h3>
        <p>{EXAMPLES[selectedTopic].description}</p>
        <pre>
          <code>{EXAMPLES[selectedTopic].code}</code>
        </pre>
      </div>
    );
  }
  return (
    <section id="examples">
      <h2>Examples</h2>
      <menu>
        <TabButton
          isSelected={selectedTopic === "components"}
          onSelect={() => handleSelect("components")}
        >
          Components
        </TabButton>
        <TabButton
          isSelected={selectedTopic === "jsx"}
          onSelect={() => handleSelect("jsx")}
        >
          JSX
        </TabButton>
        ... 생략
      </menu>
      {tabContent}
    </section>
  );
}

컴포넌트를 쪼개는 이유 2

ExamplesCoreConcepts 컴포넌트를 보면 <section /> 안에 title & content 크게 두 개의 내용을 담는 구조다. 이런 구조는 흔히 볼 수 있기 때문에 구조 자체를 컴포넌트화하는 것도 재사용성을 높일 수 있다.

따라서 아래 코드처럼 <Section> 컴포너트를 만들 수 있다.

export default function Section({ title, children, ...props }) {
  return (
    <section {...props}>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

이 때, title, children을 각각 props로 받았는데, 나머지 항목을 스프레드 형식으로 props를 가져온 이유가 있다.

만약 저 Section 컴포넌트를 적용할 경우 각각 다른 id, className, event 같은 내장된 속성들을 사용할 수 있다. 하지만 많은 built-in 속성들을 하나하나 props 값으로 적어준다면 너무 길어져 가독성이 떨어질 것이다.

그래서 내장된 속성들을 한데로 모아 section 요소에 속성들을 뿌려주면 된다!

Examples.jsx

import TabButtons from "./TabButtons.jsx";
import Section from "./Section.jsx";

export default function Examples() {
  let tabContent = <p>Please select a topic.</p>;
  return (
    <Section id="examples" title="Examples">
      <TabButtons Tab="menu">{tabContent}</TabButtons>
    </Section>
  );
}

Section 컴포넌트를 Examples에 적용해보면 위 코드처럼 쓸 수 있다.
CoreConcepts 컴포넌트도 마찬가지다!

CoreConcepts.jsx

import { CORE_CONCEPTS } from "../data.js";
import CoreConcept from "./CoreConcept.jsx";
import Section from "./Section.jsx";

export default function CoreConcepts() {
  return (
    <Section id="core-concepts" title="Core Concepts">
      <ul>
        {CORE_CONCEPTS.map((conceptItem) => (
          <CoreConcept key={conceptItem.title} {...conceptItem} />
        ))}
      </ul>
    </Section>
  );
}

정리

강의를 보면서, 저렇게까지 컴포넌트를 쪼개어 사용하는 이유가 잘 와닿지 않았었는데 블로그에 내용을 정리하면서 이해할 수 있었다👍

📌 불필요한 컴포넌트의 업데이트를 최소화하고 컴포넌트 자체의 독립성을 갖춘다.
📌 비슷한 구조의 작업들을 효율적으로 재사용성을 높이고, 일관성 있게 코드를 짤 수 있다.

막상 공부하면 이해가 됐지만 프로젝트에 직접 적용하는 것은 또 다른 것 같다..
프로젝트 규모가 클수록 이러한 컴포넌트의 장점을 적극적으로 쓰려면 작은 규모부터 하나씩 쪼개보는 연습을 할 필요가 있어보인다.

profile
한 줄이 모여 책이 되듯 기록하기

2개의 댓글

comment-user-thumbnail
2024년 6월 28일

너무 좋은 글입니다!
생각보다 많은 사람들이 컴포넌트를 쪼개지 않아요 ㅠㅠ
하지만 렌더링에 대해 잘 이해를 하고, 코드의 가독성까지 챙기려면 컴포넌트를 많이 분리하는게 좋다고 생각합니다.

1개의 답글