쏟아지는 페이지 관리 feat. Toss (+직접 구현하고 적용하기)

7

개요

토스 SLASH 23의 퍼널 : 쏟아지는 페이지 관리하기 라는 매우 흥미로운 발표 영상을 보았습니다.

토스에서는 다른 통신사들 처럼 요금제 가입 신청서를 받고있습니다.
하지만 차별화된 점은, 한 페이지로 이루어진 폼대신,
한페이지에 한 항목만 제출하는 UI를 가지고 있다는 점입니다.
정보를 슥슥 입력하다보면 가입까지 17초걸린다고 내세우고있습니다.

하지만, 이런식으로 많은 페이지들을 한번에 관리하기란 쉽지 않은데,
위 발표에서는 이러한 페이지들을 효과적으로 관리하는 방법에 대해서 설명하고 있습니다.

퍼널



토스의 가입절차는 최종단계까지 조금씩 이탈하는
깔대기같은 모양 퍼널이라는 패턴을 이용합니다. (ex. 설문조사, MBTI)

보통 이러한 플로우를 구현하려고 한다면 어떻게 할까요?

완료버튼을 이용해 router을 이동하면서
페이지마다 수집하는 유저의 정보를 전역상태에 저장하며 마지막 페이지에서 api를 호출하는 정석적인 구현입니다. (저희 회사도 이렇게 되어있습니다..!)

기존 퍼널의 아쉬운 점

완벽해 보이는 이러한 정석적인 구현방식은 몇가지 아쉬운점이 있습니다.

  1. 페이지 흐름이 흩어져있다.
    가입플로우를 파악하기 위해서 3개의 파일을 넘나들어야 합니다.
  2. 한가지 목적을 위한 상태가 흩어져 있다.
    상태를 수집하는곳과 사용하는 곳이 달라서, API에 기능을 추가하거나 버그를 수정할 때 전체 페이지를 넘나들며 데이터 흐름을 파악해야 합니다.

퍼널의 응집도를 높여보기

흩어져있는 페이지들을 한 파일에 응집시켜보겠습니다.

먼저, useState를 이용해 지역상태를 만들어주고
가입방식, 주민번호등 현재 어느 UI를 보여줘야하는지 저장합니다.


그리고는 step 상태에 따라 각 UI 컴포넌트를 조건부 렌더링하고,
'다음' 버튼을 누를때 step상태를 원하는 UI로 업데이트 해줍니다.

이로써 step의 이동을 상위에서 관리해 UI흐름을 한군데서 관리할 수 있게 되었습니다.

그리고 API호출에 필요한 상태도 상위에서 한번에 관리하면 어떤 상태가 어떤 UI에서 수집되는지 한눈에 볼 수 있습니다.

이제 더 이상 파일을 넘나들면서 전역 상태를 관리하지 않아도 됩니다.

추상화 컴포넌트로 묶어내기


조건문을 받는 Step이라는 컴포넌트를 만들어줍니다.

useFunnel

만들고보니, Step 컴포넌트가 현재 퍼널의 step을 알고 있어야합니다.
이를 위해 가입 퍼널에서 직접 관리하고 있던 step상태도 내부 로직으로 옮겨줍니다.

직접 해보기

회사에서 만들고 있던 키오스크 서비스에서는 가입뿐만 아니라 카드 등록 플로우도 존재 했는데,
입사 당시 매우 많은 플로우를 파악하기에 어려움이 있었습니다.
따라서, 이번 발표를 보고, 똑같은 로직을 적용하면 좋겠다고 생각했습니다.

먼저, 커스텀 훅을 적용하지 않고 Step 컴포넌트 먼저 구현해보았습니다.

searchParams를 이용해 ?step=가입방식 과 같은 문자열이 쿼리에 있다면,
해당 스텝으로 인식해 렌더링을 해주는 방식입니다.

다만, 이렇게 한다면

const setStep = (step: TSignUpSteps) => setSearchParams({step});

이런식으로 일일히 Step을 건너뛰는 함수와 스텝에 대한 타입을 일일히 만들어줘야합니다.

따라서, 토스의 useFunnel을 참고해 커스텀 훅과 합성 컴포넌트를 구현해보았습니다.

제네릭 타입과 커스텀훅, 합성 컴포넌트를 적절히 조합해
Step이동만을 구현한 useFunnel을 만들어보았습니다.

const [Funnel, setStep] = useFunnel(['clause', 'phone', 'auth-code', 'car-number', 'select-info', 'pin'] as const,
        							'clause',
);

이제 해당 파일에서 useFunnel안에 Step이 담긴 array와 기본 step을 지정해 사용할 수 있게 되었고,

한 파일에서 전체 UI의 흐름을 파악하고, 상태와 API도 한눈에 알아볼 수 있게 되었습니다.

	<Funnel>
            <Funnel.Step name="clause">
                <Clause
                    onEnter={}
                    onBack={goHome}
                    onNext={({hasAgreedMarketing, hasAgreedTerms}: IClauseCheck) => {
                        setRegisterData((prev) => ({
                            ...prev,
                            hasAgreedMarketing,
                            hasAgreedTerms,
                        }));
                        setStep('phone');
                    }}
                />
            </Funnel.Step>
            <Funnel.Step name="phone">
                <SignUpPhone
                    onEnter={}
                    onBack={() => setStep('clause')}
                    onNext={async (phone) => {
                        ...
                        setStep("auth-code")
                    }}
                />
            </Funnel.Step>
            <Funnel.Step name="auth-code">
                <AuthCode
                    onEnter={}
                    onBack={() => domNavigate(-1)}
                    onNext={async (authCode) => {
                        ...
                        setStep("pin")
                    }}
                />
            </Funnel.Step>
            <Funnel.Step name="pin">
                <SignUpPin
                    onNext={async (pin) => {
                        ...
                    }}
                />
            </Funnel.Step>
        </Funnel>

트러블 슈팅

사내 프로젝트에서는 react의 context api를 사용하고 있는데,
Step의 useEffect에서 context api를 호출하면 무한 리렌더링이 발생하는 문제가 있었습니다.
제가 추측하기로는, context api를 사용하면 하위 컴포넌트가 모두 리렌더링이 발생한다고 알고 있어서,
커스텀훅이 컴포넌트를 만드는 것까지 다시 렌더링이 되어 무한반복이 된것이 아닐까 생각이 듭니다.
훅에서 합성 컴포넌트를 만들때 useMemo로 감싸주어 해결하였습니다.

마치며

비록 토스를 흉내낸것에 불과하지만, 커스텀훅과 합성 컴포넌트를 만들고 구현하면서
많은 성장을 할 수 있었고, 재밌게 작업을 진행했습니다.
진유림님의 발표는 mermaid 라이브러리를 이용한 시각화 작업도 포함되어있습니다.
나중에 기회가 된다면 해당 부분까지 구현을 해보도록 하겠습니다.

Reference

배준형님의 합성 컴포넌트로 여러 페이지 다루기
Toss Slash 라이브러리 useFunnel

0개의 댓글

관련 채용 정보