합성 컴포넌트로 여러 페이지 다루기

배준형·2023년 6월 21일
4

해당 글은 Toss Slash 23: 퍼널: 쏟아지는 페이지 한 방에 관리하기를 보고 제 방식대로 정리한 내용입니다.

1. 합성 컴포넌트

합성 컴포넌트는 컴포넌트들을 조합하여 더 복잡한 UI 구조를 구축하는 데 사용되는 개념이다. 이를 통해 각 기능이나 UI 구성 요소를 독립적으로 관리할 수 있다.

일반 컴포넌트

<CustomModal
	title="title"
	content="content"
	list={[
		{
			title: "title1",
			content: "content2",
		},
		{
			title: "title2",
			content: "content2",
		}
	]}
/>

합성 컴포넌트

<CustomModal>
	<CustomModal.title>title</CustomModal.title>
	<CustomModal.content>content</CustomModal.content>

	<CustomModal.list>
		<CustomModal.listTitle>title1</CustomModal.listTitle>
		<CustomModal.listContent>content1</CustomModal.listContent>
		<CustomModal.listTitle>title2</CustomModal.listTitle>
		<CustomModal.listContent>content2</CustomModal.listContent>
	</CustomModal.list>
</CustomModal>

합성 컴포넌트 장점

합성 컴포넌트를 사용하면 코드의 재사용성이 높아지고 다양한 상황에 활용할 수 있게 되며 컴포넌트 간의 의존성이 줄어들어 유지보수가 용이해진다.

2. Funnel 컴포넌트 만들기

  • Funnel: 회원가입할 때 이름 입력, 주민번호 입력, 집 주소 입력 등 일련의 과정들을 통해 최종 목표에 이르기 까지에 대한 패턴.

※ 내용은 Toss Slash 23 발표 내용, @Toss/slash useFunnel.tsx 자료를 참고하여 작성했고, 해석은 제 기준에서 이해되는 대로 변형하여 작성했습니다. (만들어질 때의 의도와 다를 수 있습니다.)

import { Children, ReactNode, isValidElement, useState } from 'react';

interface FunnelProps<T extends readonly string[]> {
  step: T[number];
  children: ReactNode;
}

interface StepProps<T extends readonly string[]> {
  name: T[number];
  children?: ReactNode;
}

const Funnel = <T extends readonly string[]>({ step, children }: FunnelProps<T>) => {
  const validElement = Children.toArray(children).filter(isValidElement);
  const targetElement = validElement.find((child) => (child.props as StepProps<T>)?.name === step);

  if (!targetElement) {
    return null;
  }

  return <>{targetElement}</>;
};

const Step = <T extends readonly string[]>({ children }: StepProps<T>) => {
  return <>{children}</>;
};

const useFunnel = <T extends readonly string[]>(steps: T, defaultStep: T[number]) => {
  const [step, setStep] = useState(defaultStep);

  const FunnelElement = Object.assign(
    (props: Omit<FunnelProps<T>, 'step'>) => {
      return <Funnel step={step} {...props} />;
    },
    { Step: (props: StepProps<T>) => <Step<T> {...props} /> },
  );

  return [FunnelElement, setStep] as const;
};

export default function TextPage() {
  const [Funnel, setStep] = useFunnel(['가입방식', '집주소', '주민번호', '가입완료'] as const, '가입방식');
  return (
    <Funnel>
      <Funnel.Step name="가입방식">
        <h1 onClick={() => setStep('집주소')}>가입방식</h1>
      </Funnel.Step>
      <Funnel.Step name="집주소">
        <h2 onClick={() => setStep('주민번호')}>집주소</h2>
      </Funnel.Step>
      <Funnel.Step name="주민번호">
        <h3 onClick={() => setStep('가입완료')}>주민번호</h3>
      </Funnel.Step>
      <Funnel.Step name="가입완료">
        <h4 onClick={() => console.log('완료')}>가입완료</h4>
      </Funnel.Step>
    </Funnel>
  );
}

2-1) 컴포넌트 분리

interface FunnelProps<T extends readonly string[]> {
  step: T[number];
  children: ReactNode;
}

interface StepProps<T extends readonly string[]> {
  name: T[number];
  children?: ReactNode;
}

const Funnel = <T extends readonly string[]>({ step, children }: FunnelProps<T>) => {
  const validElement = Children.toArray(children).filter(isValidElement);
  const targetElement = validElement.find((child) => (child.props as StepProps<T>)?.name === step);

  if (!targetElement) {
    return null;
  }

  return <>{targetElement}</>;
};

const Step = <T extends readonly string[]>({ children }: StepProps<T>) => {
  return <>{children}</>;
};
  • Funnel: 메인 컴포넌트
    • Funnel 컴포넌트의 첫 번째 자식으로 Step 컴포넌트가 존재할 것이고, Step 컴포넌트의 props 중 name 항목이 현재 step과 일치하는지 여부를 React.Children 을 통해 확인하는 역할을 수행합니다.
  • Step: 서브 컴포넌트
    • 부모 요소인 Funnel 컴포넌트에서 Step 컴포넌트의 name props를 기준으로 어떤 컴포넌트를 렌더링할지 결정하는 요소로 사용

2-2) 메인 & 서브 컴포넌트 합성

const useFunnel = <T extends readonly string[]>(steps: T, defaultStep: T[number]) => {
  const [step, setStep] = useState(defaultStep);

  const FunnelElement = Object.assign(
    (props: Omit<FunnelProps<T>, 'step'>) => {
      return <Funnel step={step} {...props} />;
    },
    { Step: (props: StepProps<T>) => <Step<T> {...props} /> },
  );

  return [FunnelElement, setStep] as const;
};
  • useFunnel Hook으로 분리하고, 여기서 Funnel, Step 컴포넌트를 Object.assign 키워드로 합쳐 반환.
  • 사용하는 곳에서 해당 Hook을 호출한 후 반환되는 Funnel 컴포넌트, setStep 함수를 사용하면 됨.
  • 반환할 때 as const 키워드를 작성해야 사용하는 곳에서 Funnel, Funnel.Step 컴포넌트들을 인식할 수 있음.

2-3) 사용

export default function TestComponent() {
  const [Funnel, setStep] = useFunnel(['가입방식', '집주소', '주민번호', '가입완료'] as const, '가입방식');
  return (
    <Funnel>
      <Funnel.Step name="가입방식">
        <h1 onClick={() => setStep('집주소')}>가입방식</h1>
      </Funnel.Step>
      <Funnel.Step name="집주소">
        <h2 onClick={() => setStep('주민번호')}>집주소</h2>
      </Funnel.Step>
      <Funnel.Step name="주민번호">
        <h3 onClick={() => setStep('가입완료')}>주민번호</h3>
      </Funnel.Step>
      <Funnel.Step name="가입완료">
        <h4 onClick={() => console.log('완료')}>가입완료</h4>
      </Funnel.Step>
    </Funnel>
  );
}
  • useFunnel을 통해 Funnel 컴포넌트와 setStep 함수를 반환하여 사용한다.
  • 첫 번째 전달인자에 as const 키워드를 통해 타입 추론이 가능하게 하여 name, setStep을 사용할 때 자동완성이 가능하다.

참조

profile
프론트엔드 개발자 배준형입니다.

0개의 댓글