Skip to main content

Command Palette

Search for a command to run...

합성 컴포넌트 패턴에서 컨텍스트 Api

합성 컴포넌트(Compound components) 패턴에서 컨텍스트 API로 상태를 공유할 수 있습니다.

Updated
4 min read

합성 컴포넌트

합성 컴포넌트 패턴은 UI를 작성할 때 여러 개로 분리된 컴포넌트를 조합하여 컴포넌트를 개발하는 패턴이다. 컴포넌트의 조합을 활용함으로써 다양한 상황에서 컴포넌트를 도입할 수 있다.

해당 패턴으로 개발한 헤더 컴포넌트의 예시이다. 컴포넌트는 왼쪽, 가운데, 오른쪽 총 세 가지 구역으로 나뉘어져 있다.

Children.toArray 메소드로 하위 컴포넌트를 배열 타입으로 변환한 다음 각 컴포넌트의 타입을 비교함으로써 헤더 내 어디에 배치시켜야 하는지 필터링할 수 있다.

Header 컴포넌트는 HeaderContainer 컴포넌트임과 동시에 Object.assign 메소드로 타 컴포넌트들도 속성으로 추가할 수 있다.

import { Children, PropsWithChildren } from "react";

interface Props extends PropsWithChildren {
  className?: string;
}

const HeaderCenter = (props: Props) => {
  const { children } = props;

  return children;
};

const HeaderLeft = (props: Props) => {
  const { children } = props;

  return children;
};

const HeaderRight = (props: Props) => {
  const { children } = props;

  return children;
};

export interface HeaderContainerProps extends PropsWithChildren {
  className?: string;
}

const HeaderContainer = (props: HeaderContainerProps) => {
  const components = Children.toArray(props.children);
  const lefts = components.filter(
    (comp) => (comp as JSX.Element).type === (<HeaderLeft />).type
  );
  const centers = components.filter(
    (comp) => (comp as JSX.Element).type === (<HeaderCenter />).type
  );
  const rights = components.filter(
    (comp) => (comp as JSX.Element).type === (<HeaderRight />).type
  );

  return (
    <header className="w-full flex items-center justify-between gap-x-1">
      <div className="left">{lefts}</div>
      <div className="center">{centers}</div>
      <div className="right">{rights}</div>
    </header>
  );
};

const Header = Object.assign(HeaderContainer, {
  Left: HeaderLeft,
  Center: HeaderCenter,
  Right: HeaderRight,
});

작성한 헤더 컴포넌트는 다음과 같이 불러올 수 있다.

앞서 작성한, HeaderLeft, HeaderCenter, HeaderRight 컴포넌트는 Header.Left, Header.Center, Header.Right 로 호출할 수 있다.

합성 컴포넌트로써 다음과 같은 요구사항에 대응할 수 있다.

  • 만약 좌측에 있는 아이콘을 바꿔야 한다면 컴포넌트 내부를 수정하지 않고 아이콘 컴포넌트만 바꿔주면 된다.

  • 좌측에 아이콘과 텍스트를 같이 표시해야 한다면 텍스트 표시용 Header.Left 를 추가로 호출할 수 있다.

import Header from "./Header";
import LeftIcon from "./LeftIcon.svg";

const App = () => {
  return (
    <Header>
      <Header.Left>
        <LeftIcon />
      </Header.Left>
      <Header.Left>
        <h3>정보</h3>
      </Header.Left>
      <Header.Center>
        <h2>프로젝트 제목</h2>
      </Header.Center>
      <Header.Right>
        <button>업로드</button>
      </Header.Right>
    </Header>
  );
};

여러 개의 컴포넌트로 구성되는 합성 컴포넌트에서는 상태 관리를 위해 리액트 컨텍스트 API를 도입할 수 있다. 컨텍스트 API는 Props 없이 상태를 공유할 수 있게 도와준다.

컨텍스트 API로 작업한 리스트 컴포넌트이다. 선택된 텍스트와 텍스트를 클릭했을 때의 콜백 함수를 컨텍스트를 통해 타 컴포넌트로 전달할 수 있다.

ListItem 컴포넌트는 텍스트만을 Props으로 받아서 표시하는 역할을 한다. 동시에 컨텍스트로부터 콜백 함수를 받아서 사용자가 아이템을 클릭했을 때 콜백 함수를 호출할 수 있다.

import {
  Children,
  createContext,
  MouseEvent,
  PropsWithChildren,
  useContext,
  useState,
} from "react";

export interface ListContextType {
  text?: string;
  onItemClick?: (event: MouseEvent, text: string) => unknown;
}

const ListContext = createContext<ListContextType>(null!);

const ListHeader = (props: PropsWithChildren) => {
  return <div className="flex items-center">{props.children}</div>;
};

const ListSelected = () => {
  const { text } = useContext(ListContext);

  return (
    <div className="w-full flex items-center justify-center">
      <h2>{text}</h2>
    </div>
  );
};

export interface ListItemProps {
  text?: string;
}

const ListItem = (props: ListItemProps) => {
  const { onItemClick } = useContext(ListContext);
  const { text } = props;
  return (
    <button
      className="w-full flex items-center justify-center"
      onClick={(event) => {
        if (text) {
          onItemClick?.(event, text);
        }
      }}
    >
      <p>{text}</p>
    </button>
  );
};

interface ListContainerProps extends PropsWithChildren {
  onItemClick?: (event: MouseEvent, text: string) => unknown;
}

const ListContainer = (props: ListContainerProps) => {
  const { onItemClick } = props;
  const [selected, setSelected] = useState<string>();

  const components = Children.toArray(props.children);
  const header = components.find(
    (comp) => (comp as JSX.Element).type === (<ListHeader />).type
  );
  const text = components.find(
    (comp) => (comp as JSX.Element).type === (<ListSelected />).type
  );
  const items = components.filter(
    (comp) => (comp as JSX.Element).type === (<ListItem />).type
  );
  return (
    <ListContext.Provider
      value={{
        text: selected,
        onItemClick: (event, text) => {
          setSelected(text);
          onItemClick?.(event, text);
        },
      }}
    >
      <div className="flex flex-col w-full">
        {header}
        {text}
        <div className="flex flex-col w-full gap-y-1">{items}</div>
      </div>
    </ListContext.Provider>
  );
};

const List = Object.assign(ListContainer, {
  Header: ListHeader,
  Selected: ListSelected,
  Item: ListItem,
});

export default List;

시행착오

합성 컴포넌트를 어떻게 작성할지 학습하기 위해 예제 코드를 읽어봤을 때는 왜 컨텍스트를 한 번만 선언하는지 이해하지 못했던 적이 있었다.

앞서 작성한 리스트 컴포넌트는 여러 컴포넌트에서 재사용할 수 있는데 컨텍스트가 하나만 선언되어있으면 컨텍스트의 값이 계속 덮어씌워지지 않을까 우려했던 적이 있었다.

리액트 공식문서를 보고 무엇을 잘못 알고 있었는지 알 수 있었다. 컨텍스트의 값을 제공하는 역할은 컨텍스트가 아니라 컨텍스트의 프로바이더 컴포넌트(Provider)이고, 프로바이더의 value 속성으로 값을 하위 컴포넌트로 전달할 수 있다.

하위 컴포넌트에서 호출되는 useContext는 UI 트리에서 가장 가까운 프로바이더의 값을 사용하게 된다. UI 트리에 프로바이더가 없으면 createContext 함수에 전달된 기본값을 사용하게 된다.

More from this blog

AI 에이전트에서 작업 맥락을 관리하기 위해 사용한 방법

AI 에이전트의 불편함 Codex CLI와 같은 AI 코딩 에이전트를 통해서 프로젝트 개발 속도를 높일 수 있다. 그리고 혼자 작업했을 때는 놓칠 수 있는 부분도 발생할 수 있는데 AI 에이전트를 통해 보완할 수 있었다. 컴포넌트에 대해서 접근성 태그 작성 API 호출하는 비동기 코드 작성할 때 에러 처리 그리고 AI 에이전트를 잘 활용하면 불가능하

Mar 8, 202610 min read

지시사항을 분할하여 Codex로 효율적인 작업하기

이슈 Codex와 같은 AI 기반 코드 에이전트는 출력 토큰의 수가 제한되어 있다. 그래서 다양한 작업을 하나의 프롬프트에 전부 몰아서 작성하면 원하는 결과가 나오지 않을 수 있다. Codex와 같은 LLM 기반 서비스는 출력 토큰 수를 초과하게 되면 일부 단계가 누락되거나 특정 요소를 과하게 요약할 수 있고 이는 전체적인 답변 퀄리티를 낮출 수 있기 때문이다. 그래서 다양한 AI 공식문서는 특정 작업을 더 작은 단위로 분할하는 방식을 강조한다...

Nov 3, 20255 min read

Key를 활용하여 컴포넌트 상태를 초기화하기

개요 컴포넌트의 상태가 업데이트되어도 컴포넌트 내부에 배치된 컴포넌트의 상태는 그대로 유지되는 경우가 있었다. 예전에는 Props 전달 및 내부에서 useEffect를 통해서 상태를 초기 상태로 업데이트하도록 했는데 useEffect를 남용하게 되면 상태 변화 추적이 어려워지는 문제가 있었다. 그런데 useEffect 외에도 Key를 통해서 컴포넌트 상태를 초기화할 수 있다는 사실을 알게 되었다. 리스트 렌더링 React 프론트엔드에서 목록 형...

Sep 21, 20253 min read

내가 AGENTS.md를 작성하는 방법

Codex CLI 요즘 프로젝트 개발에 Codex CLI를 활용하는 이유는 다음과 같다. 특정 프로젝트를 작업할 때 Claude Code는 종종 사용량을 초과한 것과 다르게 Codex CLI는 ChatGPT Plus 요금제만으로도 토큰 사용량 초과 걱정 없이 충분하게 활용할 수 있었다. 타 CLI 기반 AI 개발 도구에 비해서 요구사항을 더 정확하게 구현하고 꼭 필요한 작업만 진행하기 때문에 코드를 검토하는 시간을 줄일 수 있다. 반복적인 코드...

Sep 21, 20254 min read
N

Nowon Lee

22 posts

합성 컴포넌트 패턴에서 컨텍스트 Api