사이퍼즈 스트리머 위젯에서 어떻게 확장하면 좋을까 고민하다가 전적 검색으로 넓혀볼까 싶었다.
사실 현재 사이퍼즈의 전적 검색을 지원하는 웹앱은 여러 가지가 있지만(심지어, 공식 홈페이지에서도 지원한다!) 최근 10판의 정보만 보여준다거나, 직관적으로 전적을 보기 불편하다거나, 광고가 너무 많고, 전적이 바로 보이지 않는다는 문제들이 산재해 있어서 이를 개선한다면 분명 수요가 존재할 것이라고 생각하고 작업에 착수했다.

react-query를 이야기하기 전에, 네오플 오픈 API에 대해서 먼저 짚고 넘어가자면, 네오플 오픈 API에 대한 요청은 보안 이슈로 무조건으로 프록시 서버를 한번 거쳐야 한다.
그래서 한번 Next.js의 기능 중 하나인 API Routes를 사용해 이를 프록시 서버처럼 운용해 이를 우회했다.
결론적으로 클라이언트에서 react-query를 통해 API Routes로 요청하고, API Routes에서 네오플 오픈 API로 요청하는데, 로 데이터를 한번 가공해서 클라이언트로 돌려주는 방식이다.

그래서 react-query에서 제공하는 useInfiniteQuery 훅을 사용하기 전엔, 무한 스크롤을 사용할 것을 생각하고 있었지만, 받을 수 있는 모든 매치 데이터를 한 번에 받아서 클라이언트로 넘겨주면 클라이언트는 그걸 그대로 매핑해 렌더하는 방식으로 구현했다.
이 방식으로 구현할 때도 애로사항이 많았는데, 클라이언트 사이드에선 별문제가 없었지만, 데이터를 한 번에 받아와야 한다는 점과 네오플 오픈 API에서 한 유저의 매칭 기록을 한 번에 전달해 주진 않는다는 점이다.

굳이 클라이언트까지 와서 결과물을 확인하고, 다음 페이지가 있다면 다시 API Routes로 요청을 보내서 다음 데이터를 받아오는 것을 반복하기보단, 결국 여러 번 네오플 API로 요청해야 하니 API Routes에서 페이로드를 확인하고, 다음 페이지가 있다면 계속 해서 요청하고, 다음 페이지가 없다면, 해당 값들을 배열에 담아 응답으로 보내주는 방식으로 처리했다.

이렇게 한 번에 받아올 때의 문제점은 당연히, 최초 로딩이 오래 걸린다.
플레이 횟수가 많지 않은 유저의 경우엔 큰 문제가 없었지만, 플레이 횟수가 많은 유저의 경우에는 페이지 로딩에서 로딩 시간이 오래 걸리는 문제가 발생했다. API Routes 단에서 재귀로 데이터를 다 받을 때까지 응답을 보내주지 않고, 한 번에 렌더되는 컴포넌트들도 많으니 당연한 문제다.
위 첨부 사진을 봤을 때 무려 3초나 스크립트 처리에 시간이 소요되고, 렌더링에도 0.7초나 소요된다.
해결.. 해야겠지?
피쳐를 기획할 때 다른 웹 앱들의 기능 중 좋다고 생각하는 것은 채용하는 방향으로 가닥을 잡았는데, 이 중 하나가 전적 검색을 할 때, 페이지네이션으로 전적을 받아올 것인가, 데이터 페칭 버튼을 두어서 클릭 이벤트로 핸들링할 것인가, 스크롤 이벤트로 핸들링할 것인가. 여러 가지를 고민해 봤으나, 페이지네이션은 전적 검색에 어울리지 않고, 클릭 이벤트의 경우 불필요한 행동이 결국 하나 더 생기는 것으로 UX를 해친다고 판단, 스크롤 이벤트를 핸들링하는 것으로 결정했다.
그런데 무한 스크롤을 만들거면서, 어째서 모든 매치를 가져오는 API Route를 만들었나요? 완전 시간 낭비 아님?
음 아니다. 네오플 API에서 제공하지 않는 정보(e.g. 자주 플레이한 캐릭터, 포지션 등)가 있기 때문에, 최근 매치들을 불러온 후, 해당 값들을 필터링 후 전달하는 과정이 있기 때문에 어차피 모든 매치들을 불러오는 것은 필요했고, 그 과정에서 무한 스크롤을 사용한 것과 사용하지 않은 것의 성능 차이가 궁금해서 겸사겸사 렌더링까지 해봤다.

글씨가 나쁜건 펜슬 필기감이 구려서 그렇다….아마도.
본론으로 돌아와서, useInfiniteQuery 훅을 사용할 때 가장 먼저 생각했어야 하는 것은 다음 페이지를 가져올 때 필요한 next key를 어떻게 API에 전달하는가, 였다.
매번 상태에 담아서 스크롤을 감지해 매번 fetch 하는 방식은 너무 react-query 스럽지 못한 방식 같았고, 클라이언트가 next key를 저장하지 않는 상태에서 이를 해결하고 싶었다.
그렇게 찾은 키워드는 pageParam 찾았다고 했지만, 공식 문서에 다 나와있다.
이를 useInfiniteQuery 훅의 옵션 중 getNextPageParam 이라는 콜백 함수를 값으로 받는 옵션이 있는데, 이 옵션의 콜백함수의 매개변수로 lastPage와 allPages가 들어오고, 이 콜백함수에서 반환하는 값이 다음 쿼리 함수 실행 시 페이지 파라미터로 들어가게 된다.
이를 통해 이전 페이지에서 전달된 next key 값을 파라미터로 넣고, 이 파라미터를 이용해 쿼리를 작성하고 요청하면, 다음 페이지 정보를 이어서 받아올 수 있는 것이다.
여기서 쿼리에 next key가 담긴 채로 API Routes로 요청하게 되면 거기에 맞는 쿼리문으로 네오플 API에 요청하는 방식으로 코드를 작성했다.
이렇게 했을 때 생긴 문제점이 하나 있다.
네오플 매칭 조회 API는 다음 페이지가 없을 경우 null 값이 응답으로 들어온다.

최초 요청은 당연히 next key가 없으니 빈 문자열로 요청하고, falsy 한 값을 확인해 API Routes에서 판별해 최초 요청, 추가 페이지 요청으로 갈리게 된다.
그런데, 마지막 페이지를 요청하는 부분에서 다음 페이지가 없으니 pageParam에 null 값이 담기게 되고, API Routes에서 falsy한 값으로 처리, 처음부터 다시 요청이 시작되게 되어버린 것이다.
처음 이 문제를 확인하고, API Route 내에서 핸들링을 해줘야 하나, 생각했다.
하지만 공식 문서를 몇 번 더 읽어보니, useInfiniteQuery 를 분해할당 했을 때, hasNextPage라는 값을 사용할 수 있는 것을 확인했고, 공식 문서상에는 undefined가 아닌 다른 값이 pageParam에 할당되면 true로 할당된다고 쓰여있었다.(A hasNextPage boolean is now available and is true if getNextPageParam returns a value other than undefined)
즉, pageParam에 undefined가 할당되면 false가 할당된다는 의미인데, falsy한 값으로 명시된 것이 아닌, undefined라고 명시된 이상, 빈 배열과 null로 담기게 되는 내 케이스에는 알맞지 않겠거니 생각했으나, 직접 사용해 보니 잘 작동하더라.
즉, undefined 뿐만 아니라 falsy한 값이 담기는 것도 false를 할당한다는 것을 알 수 있었다.

따라서 간단하게, 조건문에 해당 조건을 추가하는 것으로 해결했다.

이 모든 과정을 거쳐 다시 한번 성능을 확인해 보니, 무려 3.2초나 페이지 로딩 시간이 단축되었다.
말이 3.2초지, 4.7초에서 1.5초로 시간을 줄여버렸으니, 페이지가 완전히 로드될 때까지의 소요 시간을 75%나 단축한 것이다.

페이지 맛보기, 어딘가 익숙하다면 기분 탓이다.
사실 기능 구현에 큰 벽은 없었으나, 역시 왜 써야 하는지, 직접 찍어보며 작업하니 더 흥이 나서 작업한 것 같다.
이제 남은 작업은 공식전, 일반전 나누고, 없는 닉네임 체크하고, 404 페이지만 만들어 주면 마스터 브랜치에 올려줄 수 있을 것 같다.
이 다음엔 vercel로 배포를 한번 해보고, 성능 차이를 좀 확인해 봐야겠다.