이번 3탄은 필자가 진짜 프로젝트 기간 내내, 딥하게 파봤던 무한 스크롤을 얘기해 보고자 한다. 윈터레스트 프로젝트 회고록에서 임시 방편으로 무한 스크롤대신 페이지 네이션 방식의 원초적인 방식으로 회귀했음을 밝혔다.
그래서 필자는 리팩토링 때 이 방식을 바꿔보기로 마음 먹었다.
이전 회고에서 말씀드렸듯이, 필자의 현재 무한 스크롤은 무한 스크롤이라고 말하기에 부끄러울 정도다. 즉, 페이지 네이션이과 마찬가지의 형태로 변경되었었다.
그래서 진짜 문제가 뭔지 계속 생각해 봤다.
일단, 문제의 기능이 뭔지부터 한 번 잘 살펴보기로 했다. 문제는 스크롤을 내렸을 때 감지되도록 해 놓은 intersection observer의 제대로 된 작동이 안 되는 것이었다.
그래서 일단 코드부터 한 번 살펴봤다.
뭐가 문제일까 그래서 다른 사람들이 intersection observer를 구현한 것들을 잘 살펴봤다. 하지만, 너무나도 방법도 다양하고 특히 이렇게 필자의 사이트처럼 검색 기능이 결합된 형태를 보기는 힘들었다.
그래서 처음에는 필자는 다른 동기들에게 이 문제에 대해 한 번 문의를 해 봤다.
음... 이건 그런데, 그 서치 기능이랑은 별개의 문제로 보여요. 제 생각에는 한 번 무한 스크롤 방식 자체를 바꿔 보시는 것도 좋을 것 같아요.
그랬다. 사실 콘솔을 찍어보면 데이터 문제는 전혀 없었다.
다음과 같이 패치를 통한 데이터가 어떻게 들어오는 지 다시 확인했다.
역시 데이터 상에는 전혀 문제가 없었다. 하긴 이게 안 되면 아예 통신이 안 되는 것이라 화면이 안 뜰 것이었다. 고로, 패치 자체에는 문제가 없었다.
그래서 결국은 intersection observer의 구조 자체의 결함을 느꼈다.
먼저, useEffect안에 인터섹션 옵저버를 접근하는 entries를 콘솔로 찍어 봤다.
그랬다... 역시 여기서 isIntersecting이 false로 나오니 값이 나올리가 없었다. 하지만, 문제는 이 부분을 정확히 왜 안 되는 것인지를 잘 이해가 되지 않았다.
역시... 아직도 intersection observer에 대한 공부가 부족하다는 생각을 많이 하게 되었다.
그래서 일단, 차악보다는 차선 책을 찾는 것이 좋겠다는 판단이 섰다. 즉, 이렇게 그냥 페이지 네이션으로 남겨서 배포한다면, 유저가 엄청나게 불편한 UX를 경험할 것이 뻔했다. 그렇다면 차선이라도 택하는 편이 좋겠다는 생각에 그 간 무한스크롤에 집착해서 공부했던 기억을 더듬었다.
그리고는 구글링을 하다보니 결국 이 방법을 택하게 되었다.
결국 필자는 intersection observer api가 만들어 지기 전, 대체로 많이 활용하던 스크롤링 방식으로 다시 코딩을 해봤다. 물론 이 방식을 현재는 주로 많이 쓰지 않는 이유는 매우 잘 알고있다. 그건 후술하기로 하고, 일단 필자가 어떻게 차선을 찾았는 지 한 번 봐 주시길 바란다.
일단, fetch 구조부터 기존에 사용하던 방식으로 변경 해봤다. 뭐 이전에 사용하던 async도 문제는 없었지만, 그냥 처음 한다는 생각에 최대한 변수를 만들지 않기로 생각했다.
이전에는 스크롤 이벤트를 추가하지 않고, 맨 끝에 디브에 ref를 넣어서 그걸 감지해 줬다면, 이번 방식은 아예 사용자의 스크롤을 감지하도록 한다. 그래서 DOM에 대한 접근이 불가피했다.
이를 통해 온스크롤 시에 해당 함수를 실행하고 함수안의 조건문에 따라 리턴 값이 실행되는 구조다.
사실 코드는 별로 어렵지 않았지만, 작동하는 원리에 대해 알고 넘어가야 했다.
그래서 필자는 해당 스크롤 값들의 요소를 콘솔을 찍어봤다.
스크롤을 내리기 전의 콘솔의 모습이다.
내리고 나서의 모습이다.
차이를 아시겠는가?
자~ 자세히 보면, 콘솔 창에 수많은 값들이 찍히는데, 저게 바로 스크롤의 위치에 따른 이벤트 값들이다. 그래서 이너는 window.innerHeight, 탑은 document.documentElement.scrollTop, 오프셋은 document.documentElement.offsetHeight을 의미한다.
즉,
사진으로 보면 이와 같은데,
1) Window.innerWidth / Window.innerHeight
• 브라우저 창의 내부 너비/높이를 정수 픽셀로 반환하는 읽기 전용 프로퍼티이다.
• 스크롤바가 존재한다면 그것까지 포함한다.
• 조금 더 정확하게는, 브라우저 창의 뷰포트의 너비/높이를 의미한다.
2) document.documentElement.scrollTop
• 이 속성은 스크롤의 위치를 의미하는 속성이다.
• 따라서 초기 값은 영으로 시작한다. 이후 스크롤이 밑으로 내려가면 그 값이 증가하고, 올라가면 그 값이 감소한다.
3)document.documentElement.offsetHeight
• 스크롤바가 나타나는 부분까지의 길이다.
• 따라서 이 위의 1) +2) 의 값이 결국 offsetHeight의 값이 된다.
따라서 이런 높이 값을 이용해서 결국 이벤트를 실행시키는 방법이다.
위의 그림에서 맞닿는 지점에 결국 결국 위의 두 값이 오프셋값과 같아진다는 것을 알 수 있다. 뒤에도 페이지넘버가 늘어나면서 데이터가 10개 추가된다.
앞선 설명처럼 scrollToEnd 함수를 실행시켜서 스크롤이 바닥에 닿았을 때, 페이지 넘버의 값이 하나씩 올라가도록 추가해 줬다. 이를 통해 필자는 결국 간단하게 무한 스크롤을 해결할 수 있었다.
무한 스크롤을 구현이 되었지만, 문제가 몇 가지 발생했다.
1) 검색 기능.
역시 또 검색 기능이 말썽이었다. 스크롤이 되고나면, 검색이 다시 말썽을 부렸다. 후....
하지만, 이번에는 그동안에 경험을 토대로 바로 찾았다. 필자의 실수였다.
유즈 이펙트의 뎁스에 pageNumber만 추가한 것이었다.
바로 다시 tag까지 추가해 줬더니, 잘 시행 되었다. 유즈 이펙트는 변화 감지에 따라 시행되는 뎁스가 필요한데, 이 부분은 꼭 주의하고 사용해야겠다는 것을 다시 새삼 느낀다.
이게 사실 개발자는 구현만 된다고 다 끝이 아니라고 생각한다. 그 이후에 성능상의 이슈가 없는 지 또 체크를 해봐야 한다. 즉, 여기서도 별 문제가 없는 것 같지만, 무한 스크롤의 스크롤 방식이 왜 인기가 없어졌는 지를 잘 이해해야 한다. 즉, 스크롤 방식은 무한 스크롤을 감지하는 것은 쉽지만, 아까 필자가 말씀드린 것처럼 스크롤을 할 때마다 이벤트가 발생하는 이슈가 생긴다.
이는 사이트가 커지면서 리소스를 많이 잡아 먹는 이슈로 이어질 수 있다. 따라서 개선이 될 필요성이 있다. 물론 이를 위한 것도 역시 개발이 되어있다. 그래서 그것을 추가로 적용까지 시켜봤다.
보통 이런 리소스 누수를 방지하는 것에는 크게 2가지 방식이 있다.
1) 쓰로틀링: 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것
2) 디바운싱: 연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것
이렇게 크게 두 종류로 나뉜다. 그런데 필자가 생각하기에 현재, 스크롤 되는 우리가 적용할 무한스크롤 방식에는 디바운싱이 더 적합하다. 결국 스크롤을 내렸을 때 맨 마지막 스크롤 바닥면에서만 스크롤 이벤트가 실행되면 좋기 때문이다.
사실 원래 여기서 디바운싱 작업을 해 주기 위해 살짝 코드를 수정했다. 큰 것은 아니고, 기존의 window.onscroll 방식에서 그냥 이벤트 리스너로 등록시켜 주는 형태로 바꿔줬다.
기존과 달리 후술하겠지만, 객체에 해당 함수를 등록해줄 예정이므로 위처럼 스크롤하이트나 스크롤탑, 클라이언트 하이트로 설정해 준다. 나머지 조건식은 거의 동일하다.
이제 대망의 debounce를 구현해 줘야 한다. 일단 간단히 구조를 설명하자면, 크게 인자로는 실행될 함수와 딜레이 시간이 필요하다.
그 안의 로직은 이러하다.
타이머를 등록하고 타이머가 끝나기 전에 호출되면 타이머를 교체하는 방식으로 타이머를 계속 초기화하면서 이벤트 실행을 마지막 이벤트까지 지연시킨다. 최종적으로 지정한 시간이 초과되면 요청한 이벤트를 실행시킨다.
이제 위처럼 이벤트리스너에 debounce를 적용해 주면 된다. 안에는 적용할 이벤트 함수인 onScroll과 딜레이를 300ms로 정해줬다.
구현 결과 스크롤을 내려도 이전처럼 이벤트가 엄청 나타나는 것이 아니라, 지연 시간까지 중단되었다가 실행되는 모습을 볼 수 있었다.
사실 처음에는 그냥 무한 스크롤을 포기하려고도 했었다. 너무 생각보다 안 풀리기도 했고, 그때 마다 복잡한 문제에 직면했었기 때문에... 하지만, 포기하지 않고 그래도 차악보다는 차선을 선택하면서 구현할 수 있음에 감사하다. 또, 이번에는 성능 개선을 하는 방식까지 공부할 수 있어서 더 의미가 있었던 리팩토링 같다. 물론 아직 intersection observer를 완벽하게 구현해 보는 것까지 남은 숙제가 있지만, 그 부분도 꼭 해낼 수 있다는 자신감을 가져본다.