On a couch

[메인 프로젝트] 무한스크롤 적용하기 본문

코드스테이츠 FE/프로젝트

[메인 프로젝트] 무한스크롤 적용하기

couch 2022. 9. 19. 11:40

튜토리얼

23만이라는 어마무시한 조회수의 무한스크롤 설명 영상을 찾았다.

https://www.youtube.com/watch?v=NZKUirTtxcg 

강의를 통해 새로 배운 것

커스텀 훅 제작

(1) 커스텀훅의 초기값

usestate의 초기값으로 항상 고정된 값만 줘 왔는데, 거기에 변수를 넣어서 쓰는 건 처음 해 봤다.

초기값에  변수(categoryQuery, pageNumber)를 넣고 hook 내부에서 그걸 dependency로 설정해 두면, 변수가 변경될 때마다 hook 내부에서도 변화가 일어난다.

반대로 hook 내부에서 리턴하는 값(구조분해할당으로 받아오는 loading, error, cards, hasMore)이 변경되어도 이것을 사용하는 곳에서 리렌더링이 일어난다.

적으면서 따라가보니 저 변수들은 각각 한 곳을 참조하고 있을 테니까 다른 함수에서 변화가 일어나더라도 충분히 감지할 수 있겠다 싶었다.

 

예전엔 이렇게 값의 변화를 감지하는 과정이 이해가 안 돼서 커스텀훅을 만들다가 포기했었는데, hook의 구조를 뜯어 본 적이 없어서 그랬던 것 같다.

생각해 보면 hook도 결국은 일반적인 function을 활용한 것이고, 우리가 '초기값' 이라고 부르는 것은 그 함수의 변수, 구조분해할당으로 받아오는 것은 return 값이었던 것이다.

hook에 내가 알지 못하는 특수한 기능이 있는 게 아니었다.

 

(2) useEffect 활용

한 함수 내에서 useEffect를 여러 번 쓰는 걸 겁냈는데, 이 커스텀 훅에서는 분명히 query가 바뀌었을 때 기존 데이터를 초기화시키는 작업이 필요했다.

변수는 query와 pagenumber 두 가지이지만, 17:25 에서 query에 따라 useeffect를 한 번 쓰고 그 뒤에 query, pagenubmer에 따라 또 한 번 쓰고 있다.

순서와 논리가 맞도록 주의하면, 상황(필요한 dependency)에 따라 여러 번 분리해서 호출하는 게 깔끔한 것 같다.

 

useEffect의 CancelToken 실행

(같은 요청이 중복으로 실행될 때 이전 것을 cancle 시키는 것이라는 점은 알겠다. 그런데 마지막 요청은 왜 취소되지 않을까..?

useEffect에서 cancle()을 먼저 리턴한 후에도 마지막 axios 요청의 결과가 정상적으로 들어오는 과정을 모르겠다. 나중에 찾아보기.)

 

useCallback

https://www.daleseo.com/react-hooks-use-callback/

 

React Hooks: useCallback 사용법

Engineering Blog by Dale Seo

www.daleseo.com

지난번 프로젝트 때도 쓰긴 썼는데 왜 사용하는 것인지 몰랐다.

 

이번에 이해한 것은 2가지이다.

1. 이름 그대로 callback를 기억해서 사용하는 hook이라는 것

2. 컴포넌트 안에 있는 함수들은 사실 컴포넌트가 새로 렌더링 될 때마다 새로 정의되면서 참조값이 바뀌기 때문에, usecallback으로 참조값을 고정시켜 두지 않으면 제대로 참조되지 않을 수 있다.

=> 따라서 특정 함수를 정확히 집어다가 재사용해야 할 때 사용한다는 것.

 

intersection oberser 

https://velog.io/@yejinh/Intersection-Observer%EB%A1%9C-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

 

Intersection Observer로 무한 스크롤 구현하기

최근 react를 사용한 면접 과제에서 여러 요구사항 중 하나로 페이지네이션 구현이 있었는데 window scroll 이벤트는 여러 번 써보기도 했고 이번에 새로 알게된 IntersectionObserver 적용하여 무한 스크

velog.io

https://moon-ga.github.io/react/infinite-scroll-with-intersectionobserver/

 

[React] 무한스크롤 구현 (Intersection Observer)

무한스크롤(Infinite Scroll)이란?

moon-ga.github.io

이런 핵심 기능이 있는 것을 생각하지 못하고 이 앞단에서 '도대체 useCallback을 왜 쓰고 있는 걸까'를 고민했다.

무한스크롤 기능이 작동하는 경우는 '이미 로딩해 온 데이터 목록의 마지막에 도달했을 때'인데, 이 api는 현재 뷰포트가 어디를 보고 있는지를 observe 해준다.

애초에 이 api를 써서 '마지막 요소가 뷰포트 안에 들어갔는지' 확인하는 것이 목적이기 때문에 useRef도 쓰고 useCallback도 쓰는 것이었다.

이 영상에서는,

1.

블로그에서처럼 target ref를 state 등으로 받은 뒤 useEffect 내부에서 함수에 전달하는 것이 아니라, ref={ 함수 } 로 바로 받아서 쓰고 있다.

받아온 데이터로 element를 생성하다가 마지막 데이터를 이용해 target element가 만들어지면, 거기서 ref = { 함수 } 가 실행된다. 이때 함수의 참조값이 정확해야 하므로 useCallback 으로 감싸놓는다.

 

2.

lodaing이 false로 변할 때마다, 즉 새 데이터가 전부 불러와졌을 때마다 lastBookElementRef를 실행해 마지막 요소에 대한 감시를 시작해야 한다.
이를 위해서 dependency 배열에 loading을 넣지만, loading이 true일 때는 더 진행되지 않아야 하므로 첫 번째 줄에서 그대로 return한다.

 

3.

observer가 참조하는 ref는 스크롤과 fetching이 진행되면서 계속 변화하므로, 새 target이 만들어져서 함수가 실행될 때 이전에 만들어진 observer.current가 존재한다면 disconnect 한다.

 

4.

새로 IntersectionObserver API를 실행시켜서 observer.current에 할당한다. 이때 entries의 0번째 값(여기서는 항상 마지막 element 한 개만 감시할 테니 0번째만 검사)의 isIntersecting 속성값이 true로 변하고 && 더 가져올 데이터가 있다면 PageNumber를 다음 페이지 값으로 변화시켜서 커스텀훅을 실행시키도록 만든다.

 

5.

그 다음 새로 만들어진 target을 entries로 넘기면서 감시를 시작한다.

 

블로그를 읽은 뒤 영상을 따라 작성하면서 걱정했던 한 가지는 'root와 target이 아래로 내려갈 때 뿐만 아니라 위로 올라가면서도 교차될 수 있다고 했는데 그 부분에 대한 처리는 없나?'였다.

실제로 구현된 모습을 확인해 보니, 스크롤을 내리다가 마지막 요소에 닿는 순간 데이터 목록을 추가로 가져오고 감시 대상인 요소는 저 아래에 있는 것으로 변경되기 때문에 올라가면서 마주칠 일 자체가 안 생긴다! (닿을 듯 닿을 수 없는 그대)

더 이상 불러올 게 없는 경우에 대한 표시만 추가하면 구현을 완료할 수 있다~~


Troubleshooting ( try #1)

문제점

 

카드 목록을 동적으로 받아오면서, 각 시점에 카드 목록이 어떤 상태인지 알기 위해서 main 컴포넌트에서 콘솔을 찍어 봤다.

2번 찍히는 건 strict mode 때문이려니 하고 예사로 생각했는데 초기에 4번이 찍히고 있었다.

프리 프로젝트 때 백에서 '왜 요청이 2번씩 들어오는 지 모르겠다'는 얘기를 하셔서 메인 때는 한번 해결해봐야겠다고 생각하고 있던 참이었다.

왜 그런 것인지 멘토님께 코드를 보여드리고 여쭤봤는데 즉석에서는 해결책을 찾지 못했다.

말씀해주신 예상 원인은 (1) 커스텀훅 내부에서 useEffect를 두 번쓰기 때문이거나

(2) state로 관리하고 있는 값들이 useEffect 안에서 변화하면서 카드를 리렌더하기 때문이거나

(3) 사용중인 API의 내부 동작 원리 때문이었다.

 

과정

그래서 strict mode 끄고 매 동작마다 콘솔을 다 찍어봤다.

생각해보니 Main 32번째 줄에서 단순히 '현재 불러온 카드 목록'이 찍힌다고 해서 컴포넌트가 새로 렌더링된다는 뜻은 또 아닌 거 같아서, 로그 찍히는 위치를 map 돌려 목록 생성하는 부분에도 추가했다.

로그 찍을 당시에는 console.log가 42번째 줄에 있었다

내가 이해한 바로 각 동작이 실행되는 이유는 이렇다.

  1. 메인뷰 처음 실행 : 처음이니까 실행
    • query 상태가 변경되었으니 useEffect #1 실행
    • query와 page 상태가 변경되었으니 useEffect #2 실행
  2. 메인뷰 두번째 실행 : loading 값 변경되었으니 리턴값 받음
  3. 메인뷰 세번째 실행 : Axios 완료되어 cards(100개), loading, hasMore 변경되었으니 리턴값 받음
    • 카드 목록 렌더함
  4. 메인뷰 네번째 실행 : 뷰포인트에 마지막 요소가 보였으니 handler함수에 의해 page 상태가 변경되었음
    • 카드 목록이 리렌더됨 ← ?????????
    • page 상태가 변경되었으니 useEffect #2 실행
  5. 메인뷰 다섯번째 실행 : loading 값 변경되었으니 리턴값 받음
    • 카드 목록이 리렌더됨 ← ????????? 222222
  6. 메인뷰 여섯번째 실행 : Axios 완료되어 cards(200개), loading, hasMore 변경되었으니 리턴값 받음
    • 카드 목록 렌더함

 

로그를 전부 찍어놓고 보니 느낌이 달랐다. 안 찍어서 몰랐을 뿐 다른 자잘한 실행이 많았고, 그래서 멘토님이 주신 조언을 적용하기 애매했다.

 

가설(1) 커스텀훅 내부에서 useEffect를 두 번쓰기 때문 (x)

useEffect #1의 dependency는 [query] , #2의 dependency는 [query, pagenumber] 인데,
query는 동일하고 아래로 스크롤하면서 pagenubmer가 변화하는 상황이기 때문에
useEffect #1은 한 번만 실행되고 이후에는 영향이 없었다. 

 

가설(2) state로 관리하고 있는 값들이 useEffect 안에서 변화하면서 카드를 리렌더 (x)

이전 프로젝트 때부터 궁금했던 점인기도 한데, 한 함수 안에서 여러 state를 변경할 때의 실행 순서가 영향을 미치는 건 아닐까 싶었다.

예를 들면 setFirstState(a) 다음줄에 setSecondState(b)가 있다면, 

'훅이 setFirstState(a) 실행 후 일시정지 -> firstState에 의존하는 부분이 리렌더됨 -> 훅이 setSecondState(b) 실행 후 일시정지 -> secondState 의존하는 부분이 리렌더'이런 순서가 되는 것은 아닐까 싶었던 것이다.

노란색으로 표시한 '메인뷰 실행 ~ useEffect 리턴'이 방해 없이 쭉 이어지는 걸 보니 그건 아닌 것 같았다.

하긴 그저께 generator 함수 설명에서 JS 함수는 기본적으로 run-to-completion이랬는데 훅이 맘대로 일시정지 같은 걸 할 리가 없다....

 

가설(3) 사용중인 API의 내부 동작 원리 때문

이것은 내가 어떻게 판단할 수 있을지 모르겠다. 로그 찍은 곳에서는 힌트가 안 보인다!

 

결과

처음에 현상 파악을 잘못해서 문제 정의 자체를 잘못했다.

처음 찍은 콘솔은 사실상 '메인뷰 실행 시 찍은 로그'와 같고, 내가 진짜로 들여다봐야 하는 부분은 '카드 목록 렌더' 부분이었다.

이제 새로운 궁금증이 생기는데, 'virtual DOM은 부모가 안 바뀌거나 key값이 있으면 새로 렌더 안 한다며.. 왜 같은 값으로 계속 리렌더해...?'이다.

리액트 너 나한테 웨굴훼,,

 

Troubleshooting ( try #2)

문제점

반전의 반전. mainview가 리렌더링되면서 실행 콘솔 찍히는 것도 문제였다.

key값을 얘기하기 전에, 부모 컴포넌트가 리렌더링되니까 카드 컴포넌트들도 줄줄이 리렌더링 되는 것 같았다.

 

멘토님이 코드 확인해서 힌트 주신 거 보고 혹시나 하고 이래저래 찍어봤더니, 두 개의 useEffect에서 setState로 상태값들이 변화될 때마다 리렌더링 되는 게 맞는 거 같았다.

설마하며 상태 변화시키는 함수를 주석처리했더니 리렌더링 현상이 없어지는 것 아닌가..

초반에 일어나는 2번의 리렌더링 중 한 번은 useEffect#1에서 카드목록을 리셋하면서 생겼고,

한 번은 useEffect#2에서 loading 상태를 변화시키면서 일어나는 것 같았다.

여기서 리렌더링이 일어날 거라고 생각을 못 했던 이유는 초기상태일 때 cards는 이미 빈 배열 [ ]이기 때문이었다.

별 생각없이 이미  [ ] 인데 setCards( [ ] )한다고 뭐가 달라지겠나 했던 것이다.

그런데 setCards((prev) => {console.log(prev === [ ] ) return true}) 를 찍어보면 console에 false가 찍힌다.

당연하다... 둘이 생긴 건 같아도 참조값은 다를 거잖아....

그래서 useEffect#1에서는 cards가 빈 배열일 때 상태 변화가 일어나지 않도록 만들어서 리렌더링을 없앴다.

두 번째 영향은 loading 상태랑 hasMore 상태가 변화하는 것이다.

관계 없는 state의 변화로 cards 목록이 리렌더되는 게 거슬려서 cardlist 컴포넌트를 따로 빼고 React.memo랑 useMemo를 써서 감싸보기도 했는데 소용이 없었다.

이건 더 이상 모르겠다.. 커스텀훅에서는 axios 호출을을 하지 않는 이상 cards에 관해 수정도 하지 않는데 return하면서 cards 상태를 뱉어낼 때 참조값이 매번 달라지나보다 ㅠ

멘토님이 너무 붙잡지 말고 다른 거 하다가 돌아오면 오히려 잘 파악될 수 있을 거라고 말씀해주시기도 하셨고, 로딩 상태는 화면에 영향을 주는 게 맞는 거여서 일단은 눈감고 지나가기로 했다.

 

해결책은 못 됐지만 공부에는 도움이 됐던 관련 링크들

https://www.youtube.com/watch?v=1YAWshEGU6g 

https://leehwarang.github.io/2020/05/02/useMemo&useCallback.html

 

이제는 사용해보자 useMemo & useCallback - 이화랑 블로그

이제는 사용해보자 useMemo & useCallback 이제 useState와 useEffect에 완전히 익숙해졌다고 느꼈는데, 컴포넌트 내에서 저 두 개의 hook 만으로도 props나 state를 다루는 로직에 관련된 기본적인 기능을 모두

leehwarang.github.io

 

 


더 알아보고 싶은 것

1.

왜 새 데이터가 추가로 불러와진 뒤에 새로 렌더링 될 때도 내 페이지 스크롤은 초기화되지 않을까?

이건 react 자체의 구현 방식과 관련이 있는 것 같다.

 

2.

리렌더링 문제에 관해서 백엔드 쪽에서도 몇 가지를 검색해봐 주셨다. 하나씩 읽어봐야겠다.

 

이미 찾아보셨겠지만, 리액트의 리렌더링 이슈를 막기 위한 방식도 고정적으로 사용하는 방식이 몇개 있는 것 같네요

(1) https://velog.io/@dlwlrma/%EB%AC%B4%EC%A7%80%EC%84%B1-React-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%A7%89%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94....feat-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%8A%94-%EB%AC%B4%EC%A1%B0%EA%B1%B4-%EB%8C%80%EB%AC%B8%EC%9E%90%EB%A1%9C

(2) https://velog.io/@js43o/%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%B0%A9%EC%A7%80

(3) https://www.hamadevelop.me/rerender/

(4) https://coding0.tistory.com/56