개요
퍼널을 이탈할 때, 유저가 업데이트한 상태를 초기 상태로 clear하고 싶을 수 있다.
이런 clear 동작에 대해서 고민하다가 생각한 내용들을 정리해보았다.
TL;DR
- cleanup은 useEffect에서의 특정 행동과 pair로 관리되지 않으면 문제 상황이 발생할 수 있다.
- 컴포넌트의 side effect는 가능하다면 이벤트와 이벤트 핸들러로 관리하고, useEffect는 그렇게 하기 어려울 때 사용한다.
- 퍼널과 그에 따른 데이터 변경을 응집도 있게 관리하자.
상황 설정
모바일 커머스 앱의 결제 페이지에서 다음과 같은 상황을 예시로 생각해보자.
- 결제 수단 지정 페이지 [모바일 클라이언트 앱]
- 결제 페이지 [웹]
- 결제 페이지는 결제 수단 데이터를 Next.js의 getServerSideProps에서 Redux의 initial store로 초기화시키고 있다.
- 클라이언트 앱에서 "결제 수단 지정 페이지"를 띄운다.
- 유저가 결제 수단을 지정한다.
- 클라이언트는 "결제 페이지" 웹뷰 화면을 띄워준다. (클라이언트-웹뷰 간 데이터 전달 방식에 대해서는 생각하지 않는다)
- 최종 결제가 이루어지지 않고 웹 "결제 페이지"를 이탈한다.
- 웹뷰에서 웹 "메인 페이지"로 이동한다.
이 때, 최종 결제가 이루어지지 않았으므로, 5번의 웹 "메인 페이지"에서 결제 수단이 지정되지 않은 상태여야 하는 요구사항이 있다고 가정하자. 즉, clear 된 상태여야 한다.
이런 경우 결제 수단 정보를 clear하는 동작은 어떻게 관리해야 할까?
cleanup을 통한 솔루션
가장 먼저 떠올린 방법은 페이지 컴포넌트의 cleanup 시점에 clear시켜주는 방법이다.
1
2
3
4
5
6
7
8
9
|
function Payment() {
useEffect(() => {
return () => {
dispatch(clearPaymentActions())
}
}, [])
// ...
}
|
cs |
문제는 개발 환경에서 StrictMode 동작으로 인해 컴포넌트가 두 번 렌더되며 cleanup이 실행된다는 점이다.
결과적으로 개발 환경에서는 지정된 결제 수단 없이 퍼널이 시작되는 문제 상황이 발생한다.
이에 대한 해결책으로 두 가지를 생각할 수 있다.
- 환경변수를 참조하는 등(dev인지 확인하고 분기) 어떻게든 cleanup을 단 한 번 실행시킨다.
- useEffect가 아닌 다른 방법을 통해 구현한다.
cleanup이 솔루션이 될 수 없는 이유
내가 useEffect cleanup이 솔루션이 될 수 없다고 생각한 이유는 다음과 같다. (1번 방법으로 해결해서는 안된다!)
StrictMode 동작에도 일관된 결과가 렌더되는, 즉 본디 이펙트가 여러번 실행되어도 괜찮은 형태로 구현되어야 하는 것이 옳다고 생각했다.
그렇기에 만약 cleanup만 있는 것이 아니라 그에 페어링되는 useEffect 에서 초기화 구문이 있었다면 솔루션일 가능성이 있었다고 생각한다.
결제 페이지는 클라이언트에서 전달받은 결제 수단 데이터를 Next.js의 getServerSideProps에서 Redux의 initial store로 초기화시키고 있다.
다만 이 글 초반에 언급된 것과 같이 서버사이드에 초기화 구문이 있으므로, 클라이언트 사이드의 useEffect에 초기화 구문이 위치되지 않으므로 cleanup은 적절한 솔루션이 아니라고 생각했다.
이벤트 핸들러를 이용한 솔루션
그렇다면 다른 해결책은 무엇이 있을까?
기본적으로 리액트에서 컴포넌트는 렌더에 대해서 퓨어해야 한다. 공식처럼 계산만 해야하지, 변경해서는 안된다.
1
2
|
<Component props={1} />
<Component props={1} /> // 같은 input (props, state, context) 에 대해서 같은 결과를 리턴해야 한다
|
cs |
단, side effect가 필요한 시점이 있다. side effect는 대부분 이벤트 핸들러로 처리가 가능하다.
이벤트는 렌더 중에 일어나지 않으므로 퓨어할 필요가 없다. (Even though event handlers are defined inside your component, they don’t run during rendering! So event handlers don’t need to be pure.)
발생시켜야 하는 side effect가 그 어떤 이벤트 핸들러로도 처리가 어렵다면, 마지막 옵션으로 useEffect를 활용한다.
결론적으로, 우리가 제어하고 싶은 상황은 이벤트다. 퍼널을 이탈하는 동작은 뒤로 가기 버튼을 클릭했던지 하는 이벤트로서 처리될 수 있기 때문이다. 즉, 이펙트로 처리해서는 안되는 상황이다.
데이터를 어떻게 관리할 것인가?
그런데 문제는, 이벤트 핸들러로 클리어하는 로직들이 곳곳에 산재되어 있으면 전역 상태에 대한 관리가 힘들어진다. 어떤 퍼널에서는 어떤 데이터를 클리어해야하고, 그 후 진입 시에는 어떤 데이터가 남아 있을 것이고.. 상태 관리가 매우 복잡해질 것이다.
이 문제에 대한 해결책으로는, 한 퍼널에 해당하는 코드들을 파일 혹은 가까운 폴더에 위치시켜 응집도를 높여야 전체적인 데이터 흐름이 관리될 수 있다고 생각했다.
toss의 slash 라이브러리에 useFunnel hook이 있는데, 이 hook을 활용하는 예제 코드의 형태와 비슷한 구조로 관리되어야 데이터 흐름 관리가 되지 않을까? 생각이 들었다. 아래 코드는 slash의 useFunnel 공식 문서에서 발췌한 내용이다. 한 퍼널에 해당하는 스텝들이 응집도 있게 관리되는 모습이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
const KyoboLifeFunnel = () => {
const [Funnel, state, setState] = useFunnel(['아파트여부', '지역선택', '완료'] as const).withState<{
propertyType?: '빌라' | '아파트';
address?: string;
}>({});
const 상담신청 = useLoanApplicationCallback();
return (
<Funnel>
<Funnel.Step name="아파트여부">
<아파트여부스텝 지역선택으로가기={() => setState(prev => ({...prev, step: '지역선택', isApartment: true})} />
</Funnel.Step>
<Funnel.Step name="지역선택">
<지역선택스텝 지역선택완료={(지역정보) => setState(prev => ({...prev, step: '완료', region: 지역정보})} />
</Funnel.Step>
<Funnel.Step name="완료">
<완료스텝 신청={() => 상담신청(state)} />
</Funnel.Step>
</Funnel>
);
};
|
cs |
'개발 > React' 카테고리의 다른 글
React Chrome extension 개발을 위한 Webpack 설정 (0) | 2022.06.19 |
---|---|
React의 new JSX Transform, emotion의 css prop 설정법 (0) | 2022.06.13 |
Next.js의 Link와 Client/Server-side-rendering (0) | 2022.01.20 |
React Component npm에 publish 하기 ~ 꺽쇠, +, - 등의 아이콘 컴포넌트 (0) | 2021.08.25 |
댓글