[프론트엔드] Multi Step Form 구현과 예외 처리 (feat. useFunnel, useNavigationType, PerformanceNavigationTiming)

Woonil·2025년 5월 3일
0

프론트엔드

목록 보기
3/5

Multi Step Form이란 여러 단계로 나누어진 폼 양식을 말한다. 최근 인터넷 뱅킹이나 모바일 환경의 애플리케이션에서 회원가입을 진행하다 보면 흔히 볼 수 있는 UI이다. 우아한 테크코스의 테코톡 영상을 참고하여, 이 방식을 이전에 진행했던 프로젝트의 리팩토링에 적용해보고자 한다.

유튜브 10분 테코톡 - 수야의 Multi Step Form

Single Step FormMulti Step Form
장점개발 및 유지보수 용이, 빠른 입력 완료화면 공간 최적화, 인지 부담 감소, 집중도 향상, 조건부 질문에 유리
단점인지적 부담 ,모바일 친화성 부족, 조건부 질문에 분리구현 복잡성 증가, 복잡한 네비게이션, 전체 프로세스 파악 어려움, 컨텍스트 손실

🤔개념 & 😎실습

프로젝트의 회원가입 양식에 토스의 useFunnel을 활용하여 Multi Step Form을 적용했었다. 하지만 새로고침을 할 때, 매번 입력값이 초기화되는 문제가 있었다. 이는 사용자 경험에 치명적이라고 생각했고, 리팩토링을 통해 이러한 문제를 해결하고자 한다.

토스 라이브러리 공식문서 - useFunnel

🔃사용자가 새로고침 했을 경우 대처하기

사용자가 새로고침을 한 경우는 다음과 같을 수 있다.

  • 처음부터 재시작
  • 해당 단계만 재입력
  • 단순 실수

보통의 사용자는 회원가입을 포기하기 위해 새로고침을 하지는 않을 것이다. 띄워져야 할 주소 검색 api가 일시적인 오류로 보이지 않는 등의 의도치 않은 새로고침을 하는 경우가 일반적일 것이다. 그런데 막상 새로고침을 하니 기존에 작성하던 내용이 전부 사라져버린다면 사용자 입장에서는 허탈하거나 짜증이 날 수도 있겠다.

그렇다면 사용자가 새로고침을 눌렀을 때, 폼 상태를 유지하기 위해서 어떻게 해야할까? Web Storage API를 사용하여 쉽게 구현할 수 있다. Web Storage API란 Web API의 일종으로, 브라우저 내에 키-값 쌍을 저장 가능하게 하며 localStorage와 sessionStroage가 있다.

브라우저 종료 후에도 회원가입과 관련한 데이터가 남아있으면 보안상 적절치 않을 것이다. 따라서 임시 저장소이자 탭 단위로 상태를 저장할 수 있는 sessionStorage를 사용했다.

상태를 저장소에 저장하는 시점

상태 업데이트마다 반영

상태가 업데이트될 때마다 스토리지도 업데이트하는 방식이다. 이는 스토리지 I/O 빈도가 잦아지기 때문에 비효율적일 수 있다.

react hook form useForm()의 watch 메서드를 활용했다. watch()는 특정 input을 감시하고 필요에 따라 input의 값을 반환해주는 함수이다.

react hook form의 <FormProvider /> 를 제공하는 최상위 컴포넌트 내에서 useForm 훅을 선언한다.

<FormProvider /> 의 자식 컴포넌트에서 useFormContext 훅을 사용해 watch 함수를 불러온다. watch 함수가 반환하는 값을 formValues에 넣은 후, useEffect 훅을 통해 formValues의 변화에 따라 세션 스토리지를 업데이트한다.

새로고침 시 반영

상태를 매번 업데이트 하는 대신, 사용자가 새로고침하는 시점에 상태를 업데이트 해준다면 어떨까? 업데이트를 한 번만 수행하면 돼서 상태가 업데이트될 때마다 I/O를 수행하는 것보다 효율적일 수 있다.

그런데 새로고침하는 시점을 어떻게 알 수 있을까? 다행히도 Window의 beforeunload 이벤트는 이를 가능하게 해준다. 사용자가 현재 페이지를 떠나 다른 페이지로 이동하려 할 때나 창을 닫으려고 할 때, 핸들러에서 추가 확인을 요청할 수 있다.

Window: beforeunload 이벤트 - Web API | MDN

DOMContentLoaded, load, beforeunload, unload 이벤트

이번에는 FormSessionStorage 컴포넌트에서 react hook form의 watch 대신 getValues를 활용해 한 시점의 입력 상태값들을 가져와보자. useEffect에서는 beforeunload 이벤트에 대한 핸들러를 등록하고, 클리너 함수에서는 핸들러를 제거한다.

react-router-dom에서 제공하는 useBeforeUnload를 사용하면 아래와 같이 코드량를 줄일 수 있다.

저장소로부터 데이터를 가져오는 시점

렌더링 이후

렌더링 이후에 데이터를 가져오므로 총 두 번의 렌더링이 발생하며, 이는 화면 깜빡임이 발생하여 사용자 경험에 좋지 못하다.

렌더링 과정 중

초기 렌더링 과정 중 useState의 initializer로 폼 초깃값을 지정할 수 있다. 위에서 설명한 ‘상태를 저장소에 저장하는 시점’의 ‘상태 업데이트’ 방법을 적용할 때, react hook form useForm()defaultValues (폼의 values들에 대한 초깃값을 저장한 객체) prop을 활용했다.

🏃‍♂️‍➡️사용자가 이탈했을 경우 대처하기

그렇다면 다음 상황과 같이 사용자가 의도적으로 회원가입 과정에서 이탈하는 경우에도 위의 방법이 여전히 유효할까?

  • 앱 내 라우팅 ex) ‘홈 화면’ 버튼 클릭
  • 브라우저 좌측 상단의 ‘뒤로 가기 버튼’ 클릭
  • URL에 다른 주소 직접 입력 ex) 회원가입 중 갑자기 네이버 기사를 보러 naver.com을 입력해 이탈한 후 다시 URL로 접근

의도적으로 나갔다가 들어오는 사용자의 입력 폼에 이전에 입력했던 데이터를 넣어주는 것은 사용자의 의도와는 달라 보인다. 따라서 이러한 사용자에게는 빈 값의 폼을 보여주는 게 적절한 방법일 것이고, 이를 위해서는 스토리지의 데이터를 비워주는 작업이 선행되어야 한다.

첫 번째 단계 & 히스토리 스택 PUSH

이러한 상황을 감지하기 위해서, Multi Step Form은 하나의 페이지로 이루어져 있기 때문에 사용자가 회원가입의 첫 번째 단계로 진입 시에 히스토리 스택에 Push가 일어났는지 판단하면 된다. 즉, 사용자의 ‘새로운 계정 생성 시도’로 판정하는 것이다.

첫 번째 단계인지 판단하기 위해서는 Multi Step Form의 구현방식에 따라 다르겠지만, useFunnel의 반환값 중 하나인 currentStep을 통해 사용자가 현재 위치한 step을 알아낼 수 있었다. 히스토리 스택에 Push가 일어났는지 판단하기 위해서는 react router가 제공하는 useNavigationType 를 활용할 수 있다. 이 훅은 현재 페이지에 도달하기 위한 네비게이션 방식이 무엇인지를 알려준다.

useNavigationType v6.29.0

URL로 접근

이제 사용자가 뒤로가기/홈버튼 클릭 을 통한 접근은 판단할 수 있다. 하지만 사용자가 사이트 내 다른 페이지에 URL 입력을 통해 접근했다가, 다시 회원가입 페이지로 URL 입력을 통해 넘어온다면 여전히 세션 스토리지의 데이터가 복원된다. 이러한 원인을 파악하기 위해서는, React와 브라우저의 동작 시점의 차이를 이해해야 한다. 아래는 이와 관련한 챗지피티의 답변이다.

브라우저가 URL 입력을 통해 페이지에 접근할 때, 해당 요청은 먼저 브라우저의 기본 동작에 의해 처리되고, 그 후 React 애플리케이션이 로드됩니다. React Router는 애플리케이션이 로드된 이후 클라이언트 측 라우팅을 담당하므로, 초기 HTML 로드나 브라우저의 네이티브 내비게이션 동작에는 개입할 수 없습니다. 이 때문에 사용자가 다른 페이지에서 URL 입력을 통해 회원가입 페이지로 접근할 경우, 브라우저는 이미 세션 스토리지에 저장된 데이터를 그대로 제공하게 되며, React Router는 그 이전의 상태에 영향을 주지 못합니다.

이를 바탕으로 다시 생각해보면 브라우저 단에서 막아주는 방법을 생각할 수 있으며, 다행히도 PerformanceNavigationTiming 라는 웹 API의 type 속성이 존재했다. 이 속성은 브라우저가 실행될 때의 네비게이션의 종류를 알려주며, 그 중 navigate 값은 링크를 통한 접속, 주소창에 URL 검색을 통한 접속, 폼 제출인 경우를 의미한다. type 속성에 접근하기 위해서는 window 객체 하위의 performance 객체의 getEntriesByType() 의 인자값에 "navigation” 을 넘겨주면 된다.

따라서 아래와 같이 ‘신규 가입 시도 여부 판단’을 정의할 수 있다.

이제 URL을 통한 접근도 처리할 수 있게 되었다. 아래와 같이 회원가입 중도 이탈 후 url을 통해 다시 접근하면, 폼이 비워진 상태로 초기화된다.

지금까지 수행한 작업의 흐름을 시퀀스 다이어그램으로 표현해보았다.

🤓마무리

테코톡에서 Multi Step Form과 관련하여 설명해주신 분이 친절히 상세히 가이드를 주셔서, 문제를 빠르게 해결할 수 있었다. 물론 웹이라서 Single Step Form을 사용하고 '임시저장'과 같은 기능으로 충분히 대체가 가능할 수 있다. 하지만 Funnel 개념을 사용하여 이미 구현이 완료된 상태에서 문제를 맞닥뜨렸고, 결국 문제 해결 방법이 존재했다.

다양한 예외 상황을 어떻게 처리하느냐에 따라 사용자 경험이 좌우될 수 있다는 것을 느꼈다. 이외에도 다른 예외 케이스들도 존재할 수도 있으며, 모든 예외를 사전에 방지할 수는 없지만 설계를 꼼꼼히 할수록 서비스가 안정적으로 운영될 수 있는 것 같다.

profile
프론트 개발과 클라우드 환경에 관심이 많습니다:)

0개의 댓글