Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Funnel 컴포넌트 리팩토링 #500

Merged
merged 4 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions src/domain/RoomNew/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import { useQuery } from '@tanstack/react-query';
import { useFormContext } from 'react-hook-form';
import clsx from 'clsx';
import { steps } from '@/pages/RoomNewPage';
import { roomOptions } from '@/core/api/options';
import { FunnelHook } from '@/shared/Funnel/hooks/useFunnel';
import { useFunnel } from '@/shared/Funnel';
import { LoadingSpinner } from '@/shared/LoadingSpinner';
import { steps } from '@/pages/RoomNewPage';
import { Inputs } from '../hooks/useRoomForm';

interface NavbarProps extends FunnelHook<typeof steps> {
interface NavbarProps {
funnel: ReturnType<typeof useFunnel<typeof steps>>;
isPending: boolean;
}

const Navbar = ({
isPending,
current,
hasNext,
hasPrev,
toNext,
toPrev
}: NavbarProps) => {
const Navbar = ({ isPending, funnel }: NavbarProps) => {
const { trigger } = useFormContext<Inputs>();
const { step, hasNext, hasPrev, toNext, toPrev } = funnel;

// 참여 중인 방 정보를 가져오는 동안 다음 스텝으로 넘어가지 못하도록 합니다.
const { isSuccess } = useQuery({ ...roomOptions.myJoin() });
Expand All @@ -38,7 +33,7 @@ const Navbar = ({
return;
}

const isCompleted = await trigger(validationMaps[current], {
const isCompleted = await trigger(validationMaps[step], {
shouldFocus: true
});

Expand Down
16 changes: 8 additions & 8 deletions src/pages/RoomNewPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormProvider } from 'react-hook-form';
import { motion } from 'framer-motion';
import { useFunnel, Funnel } from '@/shared/Funnel';
import { createFunnel } from '@/shared/Funnel';
import { Header } from '@/shared/Header';
import {
BirdStep,
Expand All @@ -20,18 +20,18 @@ export const steps = [
'SummaryStep'
] as const;

const stepComponents: {
[key in (typeof steps)[number]]: JSX.Element;
} = {
const stepComponents: Record<(typeof steps)[number], JSX.Element> = {
BirdStep: <BirdStep />,
TimeStep: <TimeStep />,
RoutineStep: <RoutineStep />,
PasswordStep: <PasswordStep />,
SummaryStep: <SummaryStep />
};

const { Funnel, Step, useFunnel } = createFunnel(steps);

const RoomNewPage = () => {
const funnel = useFunnel(steps);
const funnel = useFunnel();
const { form, mutation, handleSubmit } = useRoomForm();

return (
Expand All @@ -48,7 +48,7 @@ const RoomNewPage = () => {
<main className="grow overflow-auto px-8 py-12">
<Funnel {...funnel}>
{steps.map((step) => (
<Funnel.Step
<Step
key={step}
name={step}
>
Expand All @@ -60,12 +60,12 @@ const RoomNewPage = () => {
>
{stepComponents[step]}
</motion.div>
</Funnel.Step>
</Step>
))}
</Funnel>
</main>
<Navbar
{...funnel}
funnel={funnel}
isPending={mutation.isPending}
/>
</form>
Expand Down
43 changes: 32 additions & 11 deletions src/shared/Funnel/Funnel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,52 @@ import * as FunnelStories from './Funnel.stories';

## **사용 방법**

### 1. steps 정의
### 1. createFunnel 함수 호출

먼저 createFunnel 함수를 호출해서 `Funnel`, `Step`, `useFunnel` 를 가져와주세요.
createFunnel 함수는 step 배열 정보를 일일히 제네릭으로 넘겨주지 않아도 되도록 도와주는 헬퍼 함수입니다.

step 배열에는 **as const**를 붙여서 좁은 타입으로 만들어주세요.

```tsx
const steps = ["방선택", "인증시간", "루틴정보", "비밀번호", "마무리"] as const;
const { Funnel, Step, useFunnel } = createFunnel([
'방선택',
'인증시간',
'루틴정보',
'비밀번호',
'마무리'
] as const);
```

### 2. useFunnel 훅 선언

```tsx
const funnel = useFunnel(steps, <최초로 보여줄 스텝>);
const { step, hasNext, hasPrev, setStep, toNext, toPrev } = useFunnel();
```

### 3. Funnel, Step 컴포넌트 사용

렌더링하고자 하는 Step을 Funnel 컴포넌트로 감싸주세요.
그리고 Step 컴포넌트에는 name 속성을 넘겨주세요.

```tsx
<Funnel {...funnel}>
<Funnel.Step<typeof steps> name="마무리">마무리 페이지</Funnel.Step>
<Funnel.Step<typeof steps> name="방선택">방선택 페이지</Funnel.Step>
<Funnel.Step<typeof steps> name="인증시간">인증시간 페이지</Funnel.Step>
<Funnel.Step<typeof steps> name="루틴정보">루틴정보 페이지</Funnel.Step>
<Funnel.Step<typeof steps> name="비밀번호">비밀번호 페이지</Funnel.Step>
<Funnel step={step}>
<Step name="마무리">
<div>마무리 페이지</div>
<button onClick={() => setStep('방선택')}>처음으로 이동하기</button>
</Step>
<Step name="방선택">방선택 페이지</Step>
<Step name="인증시간">인증시간 페이지</Step>
<Step name="루틴정보">루틴정보 페이지</Step>
<Step name="비밀번호">비밀번호 페이지</Step>
<div>Step 컴포넌트가 아닌 요소는 렌더링에서 무시돼요.</div>
<div>
children에 순서를 뒤죽박죽으로 등록해도 steps 배열에 들어가있는
순서로 스텝을 보여줘요.
children에 순서를 뒤죽박죽으로 등록해도 steps 배열에 들어가있는 순서로
스텝을 보여줘요.
</div>
<div>
또는 각 스텝에서 다음 스텝으로 넘어가는 로직에 setStep을 사용하고 핸들러로
전달할 수 있어요.
</div>
</Funnel>
```
Expand Down
55 changes: 28 additions & 27 deletions src/shared/Funnel/Funnel.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Funnel, useFunnel } from '.';
import { createFunnel } from '.';

const meta = {
title: 'Shared/Funnel'
} satisfies Meta<typeof Funnel>;
} satisfies Meta;

export default meta;

Expand All @@ -13,53 +13,54 @@ export const Default: Story = {
render: () => <DefaultPage />
};

const DefaultPage = () => {
const steps = [
'방선택',
'인증시간',
'루틴정보',
'비밀번호',
'마무리'
] as const;
const { Funnel, Step, useFunnel } = createFunnel([
'방선택',
'인증시간',
'루틴정보',
'비밀번호',
'마무리'
] as const);

const funnel = useFunnel(steps, '인증시간');
const DefaultPage = () => {
const { step, hasNext, hasPrev, setStep, toNext, toPrev } = useFunnel();

return (
<div className="bg-slate-100">
<div className="flex min-h-[700px] items-center justify-center text-4xl">
<Funnel {...funnel}>
<Funnel.Step<typeof steps> name="마무리">마무리 페이지</Funnel.Step>
<Funnel.Step<typeof steps> name="비밀번호">
비밀번호 페이지
</Funnel.Step>
<Funnel.Step<typeof steps> name="루틴정보">
루틴정보 페이지
</Funnel.Step>
<Funnel.Step<typeof steps> name="인증시간">
인증시간 페이지
</Funnel.Step>
<Funnel.Step<typeof steps> name="방선택">방선택 페이지</Funnel.Step>
<Funnel step={step}>
<Step name="마무리">
<div>마무리 페이지</div>
<button onClick={() => setStep('방선택')}>처음으로 이동하기</button>
</Step>
<Step name="비밀번호">비밀번호 페이지</Step>
<Step name="루틴정보">루틴정보 페이지</Step>
<Step name="인증시간">인증시간 페이지</Step>
<Step name="방선택">방선택 페이지</Step>
<div>Step 컴포넌트가 아닌 요소는 렌더링에서 무시돼요.</div>
<div>
children에 순서를 뒤죽박죽으로 등록해도 steps 배열에 들어가있는
순서로 스텝을 보여줘요.
</div>
<div>
또는 각 스텝에서 다음 스텝으로 넘어가는 로직에 setStep을 사용하고
핸들러로 전달할 수 있어요.
</div>
</Funnel>
</div>
<div className="absolute bottom-0 w-full">
<div className="grid w-full grid-cols-2">
{funnel.hasPrev && (
{hasPrev && (
<button
className="btn btn-danger col-start-1 w-full rounded-none"
onClick={funnel.toPrev}
onClick={toPrev}
>
이전으로
</button>
)}
{funnel.hasNext && (
{hasNext && (
<button
className="btn btn-success col-start-2 w-full rounded-none"
onClick={funnel.toNext}
onClick={toNext}
>
다음으로
</button>
Expand Down
35 changes: 17 additions & 18 deletions src/shared/Funnel/components/Funnel.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
import React, { PropsWithChildren } from 'react';
import { StepNames } from '../types/funnel';
import Step from './Step';
import React from 'react';
import Step, { StepProps } from './Step';

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

const Funnel = <T extends StepNames>({
current,
/** 하위 노드 중에서 렌더링 해야 할 Step 컴포넌트를 그리는 컴포넌트입니다. */
const Funnel = <T extends readonly string[]>({
step,
children
}: PropsWithChildren<FunnelProps<T>>) => {
}: FunnelProps<T>) => {
const validChildren = React.Children.toArray(children)
.filter<React.ReactElement>(React.isValidElement)
.filter((child) => child.type === Step);
.filter(React.isValidElement)
.filter((child) => child.type === Step) as React.ReactElement<
StepProps<T>
>[];

const currentStepIndex = validChildren.findIndex(
(step) => step.props.name === current
);
const currentStep = validChildren.find((child) => child.props.name === step);

if (currentStepIndex === -1) {
if (!currentStep) {
throw new Error(
'보여주려는 current 스텝이 Funnel의 children 중에 존재하지 않습니다.'
`Funnel의 children 중에서 ${step} 스텝이 존재하지 않습니다.`
);
}

return <>{validChildren[currentStepIndex]}</>;
return <>{currentStep}</>;
};

Funnel.Step = Step;

export default Funnel;
12 changes: 5 additions & 7 deletions src/shared/Funnel/components/Step.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { PropsWithChildren } from 'react';
import { StepNames } from '../types/funnel';

interface StepProps<T extends StepNames> {
export interface StepProps<T extends readonly string[]> {
/** Funnel 컴포넌트에서 렌더링 할 Step을 필터링하는데 사용됩니다. */
name: T[number];
children: React.ReactNode;
}

const Step = <T extends StepNames>({
children
}: PropsWithChildren<StepProps<T>>) => {
/** 퍼널에서 스텝 별로 렌더링 할 컴포넌트입니다. */
const Step = <T extends readonly string[]>({ children }: StepProps<T>) => {
return <>{children}</>;
};

Expand Down
28 changes: 28 additions & 0 deletions src/shared/Funnel/hooks/useFunnel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useState } from 'react';

const useFunnel = <T extends readonly string[]>(
steps: T,
initialStep: T[number] = steps[0]
) => {
const [step, setStep] = useState<T[number]>(initialStep);
const currentIdx = steps.indexOf(step);

const hasPrev = currentIdx > 0;
const hasNext = currentIdx < steps.length - 1;

const toPrev = () => {
if (!hasPrev) return;

setStep(steps[currentIdx - 1]);
};

const toNext = () => {
if (!hasNext) return;

setStep(steps[currentIdx + 1]);
};

return { step, setStep, hasPrev, toPrev, hasNext, toNext };
};

export default useFunnel;
37 changes: 0 additions & 37 deletions src/shared/Funnel/hooks/useFunnel.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/shared/Funnel/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { default as Funnel } from './components/Funnel';
export { default as createFunnel } from './utils/createFunnel';
export { default as useFunnel } from './hooks/useFunnel';
1 change: 0 additions & 1 deletion src/shared/Funnel/types/funnel.ts

This file was deleted.

Loading
Loading