개발하는 일상

Next의 router.push는 사실 동기가 아닐걸요? 본문

개발 간단 팁

Next의 router.push는 사실 동기가 아닐걸요?

롯데빙빙바 2024. 7. 17. 16:21

문제의 발견

Fake review 라는 ai가 만든 리뷰를 맞춰보는 사이트를 배포한 뒤, 사람들이 제출한 답변 데이터를 보다가 이상한 점을 하나 발견했다. 1~2초 정도의 짧은 시간 동안 같은 답변이 중복 제출되고 있는 것을 발견한 것.

그림 1. 짧은 시간내 같은 답변이 중복제출 되었다.

 

로컬 서버에서 확인하려 했을 땐 문제를 재현할 수 없었는데, 배포 환경에서 확인하니 문제가 쉽게 재현되었다(아래 그림 2). "다음" 버튼이 비활성화 되었다가, 페이지가 바뀌기 전에 활성화 되는 것이었다. 불친절한 UI 때문인지 사람들이 페이지가 바뀌기 전에 몇번씩 버튼을 눌렀나보다.

그림 2. 페이지 변경 전 다음 버튼이 다시 활성화

 

 

문제의 원인 찾기

그림 3. 제출할 때 실행되는 코드

위 그림 3은 답변을 제출할 때 실행되는 코드이다. 버튼은 상태 isSubmitting으로 제출 중에 중복 클릭되어도 제출 로직이 실행되지 않게끔 하는 의도로 짰다. 그림에는 없지만 버튼 스타일 역시 isSubmitting에 따라 다르게 보이게 했다.

 

1. 데이터가 실제 제출되며, 2. 다시 버튼이 클릭 가능해진다는 점 을 고려했을 때,  then의 콜백함수에 문제가 있다고 밖에 할 수 없었다. URLSearchParams는 네이티브한 WebAPI의 멤버이고, 동기적으로 동작한다는 것에 의심할 여지가 없다. 그렇다면 next/navigation 에서 가져온 router.push에 문제가 있을 것이다. 아래 그림 4 처럼 router.push가 promise를 리턴하지 않는다고 써있지만 말이다.

그림 4. router.push의 description. return type이 void 이다.

 

router.push가 type 정의와 달리 비동기적으로 동작한다면, 설명이 된다. 라우트 변경이 시작되고 그림 2-2의 마지막 코드 finally문이 실행되면, 라우트 변경이 완료되기 전에 버튼을 다시 클릭할 수 있게 된다. 즉, 여러번의 제출이 가능해진다.

 

vercel의 공짜 서버에 배포하였기 때문에, 속도가 로컬 환경만큼 빠르지 않을 것이라 판단하였다. 그래서 로컬에서 네트워크 스로틀링을 걸었더니 문제를 쉽게 재현할 수 있었다. 아래는 네트워크 탭을 살펴보며 문제를 재현한 영상(그림 5)이다.

그림 5. 문제 재현 with 네트워크 탭

 

네트워크 탭에 result로 시작하는 로그와 page.js 로그가 다음 화면 렌더링을 위해 하는 요청이다. 라우팅에 네트워크 요청을 포함한 일정 시간이 소요되고, 이 동안 isSubmitting 상태가 false가 되어서 다시 버튼을 클릭할 수 있게 된 것이다.

useTransition으로 문제 해결하기

문제를 해결하기 위해서는 라우팅이 완료되는 시점을 파악하는 것이 필요하다. 만약 router.push가 promise 였다면 await을 하거나 then을 쓸 수 있겠지만 그렇지 않아서 다른 방법이 필요했다. 그 방법이 useTransition을 사용하는 것이었다.

 

useTransition을 사용하면 라우팅이 진행중인지 isPending state를 통해 확인할 수 있다. 아래 그림 6과 같이 쓰면 된다. routing이 진행 중일 때 isPending이 true가 된다. 

그림 6. useTransition 활용 routing

isPending을 버튼 스타일링과 onClick핸들링에 추가하여 중복 제출되던 문제를 해결할 수 있었다.

 

Comments