해당 글은 Toss Slash 23: 퍼널: 쏟아지는 페이지 한 방에 관리하기를 보고 제 방식대로 정리한 내용입니다.
합성 컴포넌트는 컴포넌트들을 조합하여 더 복잡한 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>
합성 컴포넌트 장점
합성 컴포넌트를 사용하면 코드의 재사용성이 높아지고 다양한 상황에 활용할 수 있게 되며 컴포넌트 간의 의존성이 줄어들어 유지보수가 용이해진다.
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>
);
}
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를 기준으로 어떤 컴포넌트를 렌더링할지 결정하는 요소로 사용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
키워드로 합쳐 반환.Funnel
컴포넌트, setStep
함수를 사용하면 됨.as const
키워드를 작성해야 사용하는 곳에서 Funnel
, Funnel.Step
컴포넌트들을 인식할 수 있음.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
을 사용할 때 자동완성이 가능하다.