이탈률에 대한 고민은 여전하다. 어떻게 하면 귀찮은 회원가입이나, 설문조사와 같은 단계에 있어서 서비스 이용자가 포기하지 않고 서비스를 지속해서 사용할 수 있을지에 대한 고민은 여전하다. 행운이지만, Toss는 이런 고민을 하는 서비스 회사라는 것이 다행이다.
혼자서 진행하던 사이드 프로젝트에서, Toss의 use-funnel 을 사용해보고 싶었고, 운이 나쁜건지 (결국 해냈지만) React Native와의 연동 과정에서 험한 꼴을 당했지만 결국 해결해낸 과정을 서술한다. 2번의 과정에 걸쳐서 작성해야할 것 같다.
1편에서는 use-funnel의 정의와 사용 목적에 대해서 알아보고, 2편에서는 내가 React Native에서 어떻게 사용했는지 알아본다.
Toss에서 만든 React 기반의 상태 관리 라이브러리
회원가입, 주문 등 단계 기반 플로우의 타입을 안전하게 관리한다.
장점
npm install @use-funnel/core
@use-funnel/browser - 브라우저 기반
@use-funnel/react-router - React Router 사용 시
@use-funnel/next - Next 사용 시
@use-funnel/react-navigation-native - React Native
type SignupFunnel = {
Email: { email: string };
Password: { email: string; password: string };
Profile: { email: string; password: string; name: string };
Confirm: { email: string; password: string; name: string; agreed: boolean };
};
딱 봐도 느낌이 오듯이, 각 단계 이름과 해당 단계에서 사용할 상태 (context)를 명시한다. step 전환시마다 context를 누적하여 넘기고, 이에 타입 안전성을 보장받을 수 있다.
import {
useFunnel,
FunnelProvider,
FunnelStep,
useFunnelContext,
} from '@use-funnel/browser';
type SignupFunnel = {
Email: {};
Password: { email: string };
};
export default function SignupFlow() {
const [funnel, setFunnel] = useFunnel<SignupFunnel>({
id: 'signup-funnel',
initial: { step: 'Email', context: {} },
});
return (
<FunnelProvider value={{ funnel, setFunnel }}>
<FunnelStep name="Email">
<EmailComponent />
</FunnelStep>
<FunnelStep name="Password">
<PasswordComponent />
</FunnelStep>
</FunnelProvider>
);
}
useFunnel<SignupFunnel>() : step과 context 를 관리하며 상태 전환 함수(setFunnel)을 반환SignupFunnel : 각 단계 (step)의 이름과 해당 단계에서 필요한 context 타입을 정의한 TypeScript 객체FunnelProvider : 상태 (funnel, setFunnel)를 하위 컴포넌트에 전달하는 React Context ProviderFunnelStep name="name.." : 현재 step 값이 name과 일치할 때에만 자식 컴포넌트를 렌더링함useFunnelContext : 하위 컴포넌트 (email, password) 에서 현재 funnel 상태와 setFunnel 을 가져올 수 있게 해줌id : 내부적으로 히스토리 상태 저장 시 유일한 키값 으로 사용됨. ex) signup-funnel, pay-funnel 등 여러 퍼널을 동시에 운영하는 경우 id가 중복되면 안되기에 반드시 고유하게 설정해야함.import React, { useState } from 'react';
import { useFunnelContext } from '@use-funnel/browser';
function EmailComponent() {
const { setFunnel } = useFunnelContext<SignupFunnel>();
const [email, setEmail] = useState('');
const handleNext = () => {
// 간단한 유효성 검사
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
if (!isValid) {
alert('올바른 이메일 형식을 입력해주세요.');
return;
}
// context에 email 저장 후 Password 단계로 이동
setFunnel({
step: 'Password',
context: { email },
});
};
return (
<div>
<h2>이메일 입력</h2>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="example@domain.com"
/>
<button onClick={handleNext} disabled={!email}>
다음
</button>
</div>
);
}
export default EmailComponent;
// context에 email 저장 후 Password 단계로 이동
setFunnel({
step: 'Password',
context: { email },
});
};
이 부분을 자세히 확인하면 되는데 다음 funnel 단계 (password)를 위해 context에 email을 setFunnel을 통해 저장한 모습이다.
이렇게 context에 저장된다면, 뒤로가기/앞으로가기 동작 시 유지된 context의 내용을 확인할 수 있다.
history.pushState를 사용하거나, react-router를 연동하여 사용할 수 있다. (URL query 혹은 pathname으로 동기화)