> ## Documentation Index
> Fetch the complete documentation index at: https://grit03.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# [CH4] 리액트 컴포넌트 설계하기

## 단일 책임 원칙

단일 책임 원칙(Single Responsibility Principle, SRP)는 함수, 클래스 혹은 리액트 컴포넌트는 변경해야할 이유가 단 하나만 있어야 함을 의미한다.\
→ 즉, **하나의 컴포넌트는 하나의 작업이나 기능을 수행하는 것**이 이상적이다.

> [!NOTE]
>
> **장점**
>
> * 코드 가독성 향상
> * 코드 유지보수성 향상
> * 코드 테스트 및 디버깅 쉬워짐

### 예시

블로그 포스트 컴포넌트는, 블로그 포스트 데이터 가져오기, 블로그 포스트 표시, 좋아요 기능 관리 까지 3가지 동작을 분리해서 리팩토링할 수 있다.

```tsx title="useFetchPost.tsx" theme={null}
// 블로그 포스트 데이터 가져오기
import { PostType } from "./types";
import { useEffect, useState } from "react";
import fetchPostById from "./fetchPostById";

const EmptyBlogPost = {
  id: "",
  title: "A title",
  summary: "A short summary of a title",
};
export const useFetchPost = (id: string): PostType => {
  const [post, setPost] = useState<PostType>(EmptyBlogPost);

  useEffect(() => {
    // @ts-ignore
    fetchPostById(id).then((post) => setPost(post));
  }, [id]);

  return post;
};
```

```tsx title="BlogPost.tsx" theme={null}
// 블로그 포스트 표시
import React from "react";
import { useFetchPost } from "./useFetchPost";
import { LikeButton } from "./LikeButton";

const BlogPost = ({ id }: { id: string }) => {
  const post = useFetchPost(id);

  return (
    <div>
      <h2>{post.title}</h2>
      <p>{post.summary}</p>
      <LikeButton />
    </div>
  );
};

export default BlogPost;
```

```tsx title="LikeButton.tsx" theme={null}
// 좋아요 기능 관리
import React, { useState } from "react";

export const LikeButton: React.FC = () => {
  const [isLiked, setIsLiked] = useState(false);

  const handleClick = () => {
    setIsLiked(!isLiked);
  };

  return <button onClick={handleClick}>{isLiked ? "Unlike" : "Like"}</button>;
};
```

## 중복 배제 원칙 (DRY)

중복 배제 원칙(Don't Repeat Yourself, DRY)은 코드 안에서 중복을 줄이는 것이 목적이다.
→ 중복을 최소화하고, 재사용성을 높여준다.

> [!NOTE]
>
> **장점**
>
> * 코드 가독성 향상
> * 코드 유지보수성 향상
> * 코드 테스트 쉬워짐
> * **로직 중복으로 인해 발생하는 버그를 방지할 수 있음**

### 예시

상품 리스트를 보여주는 `ProductList` 컴포넌트는 상품 이미지와 상품명, 가격, 장바구니 추가 버튼을 표시한다.\
장바구니 리스트를 보여주는 `CartList` 컴포넌트 역시 상품 이미지, 상품명, 가격, 장바구니에서 제거 버튼을 표시한다.\
구조가 매우 유사하다. 이런 경우, **중복된 구조를 하나의 컴포넌트로 만들고, 그것을 사용하도록 설계**할 수 있다.

```tsx title="LineItem.tsx" theme={null}
// 상품 리스트를 보여주는 LineItem 컴포넌트
import { Product } from "../types";

const LineItem = ({
  product,
  performAction, // 버튼 클릭 시 호출되는 함수
  label, // 버튼 라벨
}: {
  product: Product;
  performAction: (id: string) => void;
  label: string;
}) => {
  const { id, image, name, price } = product;

  return (
    <div key={id} className="product">
      <img src={image} alt={name} />
      <div>
        <h2>{name}</h2>
        <p>${price}</p>
        <button onClick={() => performAction(id)}>{label}</button>
      </div>
    </div>
  );
};

export default LineItem;
```

이 컴포넌트를 활용해 상품 리스트를 보여주는 `ProductList` 컴포넌트와 장바구니 리스트를 보여주는 `CartList` 컴포넌트를 구현한다.

이렇게 구성하면, 상품 리스트 관련 변경은 한 곳(`LineItem`)에서만 일어나므로 버그가 발생할 가능성이 줄어든다.
그리고 `LineItem`에 새로운 기능을 추가한다면, 하나의 컴포넌트만 수정하면 된다.

> 기능 변경을 한 곳에서 해결할 수 있기 때문에, 유지보수가 단순해질 수 있다.

## 합성 활용하기 (컴포넌트 합성 원칙)

단순하고 재사용 가능한 컴포넌트를 **조합**하여 복잡한 UI를 구성하는 것이다.\
리액트에서는 **상속보다 합성(composition)** 을 선호한다.

### 예시

```tsx title="UserDashboard.tsx" theme={null}
// 사용자 대시보드 컴포넌트
import { UserDashboardProps } from "./types";
import { UserProfile } from "./UserProfile";
import { FriendList } from "./FriendList";
import { PostList } from "./PostList";

function UserDashboard({ user, posts }: UserDashboardProps) {
  return (
    <div>
      <UserProfile user={user} />
      <FriendList friends={user.friends} />
      <PostList posts={posts} />
    </div>
  );
}

export default UserDashboard;
```

위 코드에서는 `UserDashboard` 컴포넌트가 `UserProfile`, `FriendList`, `PostList` 컴포넌트를 합성하여 사용자 대시보드를 구성하고 있다.

> [!NOTE]
>
> **위와 같이 합성 컴포넌트를 사용했을 때의 장점**
>
> * **관심사 분리**
>   * 컴포넌트의 여러 다른 영역을 `UserProfile`, `FriendList`, `PostList` 각 컴포넌트로 분리하여, 하나의 컴포넌트가 하나의 역할을 담당하게 되서, 유지보수가 쉬워진다.
> * **더 나은 가독성**
>   * 각 컴포넌트가 어떤 것을 렌더링할지 명확하게 보이기 때문에 가독성이 높다.
> * **높은 재사용성**
>   * `UserProfile`, `FriendList`, `PostList` 컴포넌트를 애플리케이션의 다른 영역에서도 재사용할 수 있기 때문에 중복을 줄일 수 있다.

> 합성 컴포넌트의 핵심은, 재사용이 가능한 작은 컴포넌트를 결합하여 큰 컴포넌트를 만든다는 것!

## 컴포넌트 설계 원칙의 결합

실전 코딩 상황에서는, 위와 같은 원칙들을 동시에 적용해야 한다.

헤더, 사이드바, 본문 영역의 상태와 움직임을 관리하는 `Page` 컴포넌트를 생각해보자.

```tsx title="Page.tsx" theme={null}
function Page({
  headerTitle, // 헤더 제목
  headerSubtitle, // 헤더 부제목
  sidebarLinks, // 사이드바 링크
  mainContent, // 본문 영역
  isLoading, // 로딩 상태
  onHeaderClick, // 헤더 클릭 시 호출되는 함수
  onSidebarLinkClick, // 사이드바 링크 클릭 시 호출되는 함수
}: PageProps) {
  return (
    <div>
      <header onClick={onHeaderClick}>
        <h1>{headerTitle}</h1>
        <h2>{headerSubtitle}</h2>
      </header>
      <aside>
        <ul>
          {sidebarLinks.map((link) => (
            <li key={link} onClick={() => onSidebarLinkClick(link)}>
              {link}
            </li>
          ))}
        </ul>
      </aside>
      {!isLoading && <main>{mainContent}</main>}
    </div>
  );
}
```

`Page` 컴포넌트는 많은 속성을 가진 prop으로 여러 가지 역할을 하며, **컴포넌트가 보통 5개 이상의 prop을 가진다면 분리가 필요**하다.\
→ 각 prop의 용도를 기억하기 쉽지 않을 수 있고, 잘못된 prop을 전달할 수 있다.

커다란 컴포넌트를 작게 나누는 방법은 다양하다. 한가지 방법은, 정보가 서로 관련되어 있다면, 하나의 그룹으로 묶고, 이 단위로 새로운 컴포넌트를 만든다.

```tsx title="Page.tsx (분리 후)" theme={null}
function Page({
  headerTitle,
  headerSubtitle,
  sidebarLinks,
  mainContent,
  isLoading,
  onHeaderClick,
  onSidebarLinkClick,
}: PageProps) {
  return (
    <div>
      {/* 헤더 컴포넌트 */}
      <Header
        title={headerTitle}
        subtitle={headerSubtitle}
        onClick={onHeaderClick}
      />
      {/* 사이드바 컴포넌트 */}
      <Sidebar links={sidebarLinks} onLinkClick={onSidebarLinkClick} />
      {/* 본문 컴포넌트 */}
      <Main isLoading={isLoading} content={mainContent} />
    </div>
  );
}
```

작은 컴포넌트로 나누고 합성을 해서, 훨씬 가독성이 좋아졌다.\
하지만, 이 컴포넌트는 완벽하지 않다. **`Sidebar` 컴포넌트나, `Main` 컴포넌트에** 새로운 기능을 추가해야 해서 **prop을 추가**해야 한다면, **`Page` 컴포넌트도 수정해야 한다.**\
그러면, **`Page` 컴포넌트의 테스트도** 늘어난 prop에 맞게 **수정해야**하므로, 상황이 상당히 복잡해진다.

`Header`나 `Sidebar` 컴포넌트의 prop을 모두 표시하지말고, **각 컴포넌트의 인스턴스를 prop으로 전달받아 적절한 위치에 렌더링**하도록 바꿔주자!

```tsx title="Page.tsx (prop 전달 방식 변경 후)" theme={null}
function Page({ header, sidebar, main }: PageProps) {
  return (
    <div>
      {header}
      {sidebar}
      {main}
    </div>
  );
}
```

```tsx title="MyPage.tsx" theme={null}
const MyPage = () => {
  return (
    <Page
      header={
        <Header
          title="My application"
          subtitle="Product page"
          onClick={() => console.log("toggle header")}
        />
      }
      sidebar={
        <Sidebar
          links={["Home", "About", "Contact"]}
          onLinkClick={() => console.log("toggle sidebar")}
        />
      }
      main={<Main isLoading={false} content={<div>The main</div>} />}
    />
  );
};
```

`MyPage` 컴포넌트는 `Page` 컴포넌트의 prop으로 각 컴포넌트의 인스턴스를 전달받아 렌더링하고 있다.

`Page` 컴포넌트의 초기 버전은 **지나치게 많은 역할**을 맡고 있어서 prop 역시 늘어날 수 밖에 없었다.
그 후 개선한 버전에서도 많은 데이터를 각 계층에 전달해야하는 **props drilling 문제**가 있었다.\
**→ 이런 설계는 구성하기 복잡하고, 유지보수하기 어렵다.**

> [!NOTE]
>
> **정리**
>
> **단일 책임 원칙을 기반으로, 거대한 `Page` 컴포넌트를 작은 컴포넌트로 분리하였다.**\
> → 각 컴포넌트는 하나의 역할만 담당할 수 있게 된다.
>
> **그리고 합성을 통해 `Page` 컴포넌트가 분리한 서브 컴포넌트를 prop으로 전달받도록 수정했다.**\
> → props drilling 문제도 해결되었다.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.