때는 바야흐로 면접일정이 하루에 두개씩 잡히며 전날 갑작스럽게 "님 내일 면접 ㄱ?" 를 받으며 바쁜 날을 보내던...기간이 드디어 끝난줄 알고 후후 타입스크립트랑 다른거좀 공부하고 나의 이 비루한 블로그의 임시저장글들을 완성시켜볼까나..! 하던 목요일.
엄청귀염뽀짝해보이는걸 만드는 회사에서 과제를 주셨다. 과제 내용은 요약하자면 "제공한 서버 주소를 사용하여 무한스크롤을 이용한 페이지네이션으로 리스트를 받아서 노출시키는 사이트" 를 만드는.....것!
그래서 에이 머야 그럼 이전 플젝에 썻던 무한스크롤 써버리면 1시간 컷 순삭 쑝가능 아니냐구~~~?
응 아님.
이 누추한 내게 이런 과제..심지어 기간을 5일이나 주신거면 똑같은거 쓰지말고 새로운거를 쓰거나 하라는게 아닐까?
심지어 나의 포폴을 보셨다고 했는데..!
원래 같은 맛 또 먹는 것보다 다른 맛 먹는게 더 신박하게 느껴지는법. 새로운거를 시도하자!!!
그래서 참고로 기존에 썼던 무한스크롤 방식
저게..뭐냐면.. 일단 axios 관련 코드쪽이 좀 지저분한건 둘째치고,
onScroll 이벤트를 통해 스크롤이 일어날때마다 scrollHeight, scrollTop, clientHeight 를 가져오는 방식인데..!
일단 솔직히 이거 너무 싫었다. 이유는?
스크롤할때마다 저걸 반복하는건데 좀 더 효율적이게 하려면 또 머 throttle 같은거 써야하는데 암만 생각해도 이렇게 발달한 세상에, 무한스크롤이 거의 웹사이트의 기본이 된 세상에 좀 더 효율적인게 있을게 분명해서...!
암튼 이거 쓰기 싫어서 딴거 찾고있다고 게더에서 같은 부트캠프 달렸던 별명 '독거노인 아저씨'(줄여서 '아재')에게 궁시렁거리니 이런걸 알려줬다.
IntersectionObserver
이전에 무한스크롤 써먹는 방법 찾다가 보긴 했는데 그땐 무한스크롤에 대한 기본 지식 자체가 부족해서 조금 올드한 위의 방식을 써먹던거였는데, 드디어 열어볼때가 왔다. 인터섹션옵저버..! 발음 이거 맞나?
여튼, 이걸로 뭘 할 수 있을까?!
우선 모를땐 구글링..인데 블로그 포스터로 본걸로는 약간 아리송했다. 그래서..얘가..머..어..어따써먹는건데!?
그래서 유튜브 검색하고 이것저것 보다가 이런..엄청난..꿀강의 발견.
https://www.youtube.com/watch?v=nE4zY6PY748
무한스크롤을 하는 코드짜는걸 그대로 보여준다.
여기에서 사용하는 인터섹션 옵저버의 역할은 정말 단순하다.
'특정 엘레먼트가 화면에 보여진 순간'을 포착해서 내가 원하는 행동을 실행시킨다!
이게 끝.
아래는 내가 가볍게 뼈대만 만들어서 짜낸것.
더보기 버튼이 보이는 순간 서버에게 요청을 보내 다음 리스트를 불러와 기존 리스트에 추가한다.
만약 리스트를 전부 다 불러와서 404 에러가 한번 뜬다면 그 이후로는 더보기버튼이 보이지 않게 변경한다.
참고로 더보기버튼은 무한스크롤 적용 전에 서버에 다음 리스트를 불러올 수 있는지 확인하기위해 넣었었음.
사용법은 아래로 쭉 적어놓는다!
우선 어떤걸 state로 저장해야할까?
1. 리스트 내용물
2. 페이지 번호
3. 서버에 요청을 주었는가? true/false
4. 서버에서 받아올 리스트가 더 있는가 없는가? true/false
const [list, setList] = React.useState([]);
const [page, setPage] = React.useState(1);
const [loading, setLoading] = React.useState(false);
const [isList, setIsList] = React.useState(true);
이때, 서버와의 요청을 적는 코드는 사람마다 다르니까 굳이 안적음. 필요한것만 적자면..!
배열, 리스트 형식의 data 를 서버에서 받아와 data 라는 이름으로 선언을 내렸다면
setList((prev) => [...prev, ...data]);
이렇게 적어주면 된다.
prev 는 이전에 state인 list에 넣어져있던 데이터의 배열을 말하는거고 ... 을 통해 내용물을 꺼내놓은 것,
...data 를 적어 data 안의 내용물도 똑같이 꺼내 두개를 합친 새로운 배열을 생성해서 setList 를 이용하여 list에 저장한것..! 이당.
비슷하게 페이지 번호가 변하는 것도 아래와 같이 적으면 된다. 기존 번호에 +1을 한다는 식이다.
setPage((prev) => prev + 1);
이 다음은 어떻게 하면 될까? 이 이야기를 하기 전에 나의...이전 코드, 맨 위에 이미지로 넣은 코드를 보면 좋다.
다시봐도 구리다. 본 김에 이 글 쓰고 개선좀 해야겠다....
왜 구린가? 고인물개발자분들이 보면 내가 생각하는것 이상의 구림을 느낄 수 있으시겠지만 일단 내가 느낀 구림중 가장 큰건 다음과 같다.
페이지 번호 제대로 setState 안되니까 setTimeout 까지 넣었는데 그래도 업데이트가 안되니까 페이지 넘버를 업데이트하는걸 짜놓고선 정작 서버에 페이지 숫자 요청 보낼때는 state가 아니라 그냥 직접 받아온 페이지 넘버를 때려박는 비효율의 극치를 보여주는 코드..!!!
지금 보니 현기증난다. 이 코드를 보셨을 모든 개발자분들께 사죄의 말씀 올립니다.
여튼 이 얘기를 왜 했냐면, 저게 저 꼬라지가 난 이유는 내가 저때만 해도 아직 useEffect 를 제대로 활용하고 있지 못했기 때문이다.
물론 지금이라고 완벽하게 쓴다고 자신하지 못하지만..!
useEffect를 쓰는 이유중에 하나가 '원하는 state가 업데이트 되었을 때'를 감지하고 그 때에만 특정 행동을 일으킬 수 있다는 것..!
그럼 내가 위에 적은것처럼 페이지넘버를 업데이트하고 바로 아래에 서버 연결하는 코드를 적고 어쩌구저쩌구 하지 말고,
useEffect를 사용해 페이지 넘버가 업데이트 되었을 때만 서버에 요청이 가서 response 를 받아와 기존 리스트에 추가하면 된다!
이렇게 말하면 나 같은 사람들은 솔직히 몬소린지 못알아먹을거같으니 코드 아래에 적어두자..
React.useEffect(() => {
getList(page);
}, [page]);
getList는 axios를 이용해 서버에 요청을 보내는 함수를 넣은 함수이다!
page 는 페이지 넘버이고.
useEffect는 페이지 넘버가 변경되었을 때만 getList(page)를 실행하기때문에
위의 나의 구린 코드처럼 setState를 이용한 업데이트가 끝나기전에 서버에 요청이 가는 불상사가 벌어지지 않고
페이지넘버가 '반드시'업데이트된다! 와! 샌즈!
그럼 이제 인터섹션옵저버를 이용해 특정 엘리먼트가 뷰에 보일때 일어나는 행동에 setPage((prev) => prev + 1); 를 넣으면 된다.
그래서 그 인터섹션옵저버를 어케 쓰냐고!
드디어 적는다. 이 글을 적게된 원인. 원래 TIL로 소소하게 적으려다가 나 같은 사람이 좀 더 보기 편하길 바라는 쟈근마음으로 적는...그것!
일단 또 useEffect를 적어야한다. 이 새로운 useEffect가 감지할 state는 loading 이다.
미리 적어둔 서버와의 연결하는 행동을 맡은 함수 getList의 끝자락에 setLoading(true)를 넣어준다.
이러면 getList 가 실행될때마다 새로운 useEffect 가 실행된다.
이제 이 useEffect안에 다음과 같은 코드를 넣는다.
if (loading) {
const observer = new IntersectionObserver((e) => {},{ threshold: 1 });
}
threshold 의 의미는 MDN에서 아래와 같이 적어놓았다.
observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. 만일 50%만큼 요소가 보여졌을 때를 탐지하고 싶다면, 값을 0.5로 설정하면 됩니다. 혹은 25% 단위로 요소의 가시성이 변경될 때마다 콜백이 실행되게 하고 싶다면 [0, 0.25, 0.5, 0.75, 1] 과 같은 배열을 설정하세요.기본값은 0이며(이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다). 1.0은 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미합니다.
그니까 쟤가 1이면 내가 옵저버의 타겟으로 정해둔 컨텐츠가 전부 보여야 원하는 함수를 실행시켜주는거고,
0.5면 절반만 보여도 실행, 0이면 진짜 0.0000000..0001만큼만 보여도 실행해준다는거!
그럼 이제 타겟을 정해주자.
타겟은 아무거나 해도 상관은 없는데 무한스크롤이벤트가 필요한 리스트들의 맨 아레에 넣어주고 useRef로 ref값을 지정해주자.
타겟이 정해졌으면 옵저버에게 내가 정한 타겟이 타겟이라는걸 알려줘야한다.
if (loading) {
const observer = new IntersectionObserver((e) => {},{ threshold: 1 });
observer.observe(ref.current);
}
.observe에 ref.current를 넣어 사용하면 옵저버가 타겟을 인식하게 된다.
그럼 이제 타겟이 뷰에 들어왔을때 뭘 할지 정하면 된다.
그럼 일단 console.log(e)를 찍어보자!
뭐가 많이 나오는데..
우선 위에 찍힌건 처음 컴포넌트가 뷰에 떴을때이고 아래에 찍힌건 스크롤을 내려서 타겟이 뷰에 들어갔을때 콘솔에 찍힌것이당.
뭐가 다른가 하면 많이 다르긴한데, 우리가 주목해야할건 isIntersecting 이다!
얘는 false 였다가 뷰에 포착되면 true로 변경된다. 인터섹팅도 마침 '교차'라는 의미를 가졌으니..!
킹-리적 갓심으로 이건 타겟이 뷰에 100% 겹쳐졌는지 아닌지를 boolean으로 표현하는거!!!!! 라고 추측한다.
다른거는 뭐지..? 싶은데 일단 넘어가기로 하자.
그럼 내가 할건 isIntersecting이 true 일때 setPage((prev) => prev + 1); 를 실행시켜주면 위에서 처음 만든 useEffect가 페이지넘버의 변화를 감지하고 getList(page)를 실행하도록 만들어주면 되는거..쉬다~~~~!!!!!
if (loading) {
const observer = new IntersectionObserver((e) => {
const isFocusEnd = e[0].isIntersecting;
if (isFocusEnd) {
setPage((prev) => prev + 1);
}
},{ threshold: 1 });
observer.observe(ref.current);
}
이러면 끝~~!!!!!
..엥, 근디 그럼 loading을 왜 state로 저장해둔거고 왜 굳이 저걸 useEffect에 넣어두는겨? 걍 한번 지정했으면 끝인거 아님?
응 아님.
나의 첫 IntersectionObserver 는 useEffect에 옵저버를 선언하지 않고 그냥 밖에다가 선언했었는데, 제대로 실행되질 않았다.
이유가 뭘까? 싶었는데 아까 e를 콘솔에 찍어보고 킹리적갓심이 또 등장했다.
이 옵저버는 우리가 상상하던것처럼 엘레먼트를 완전 편하게 콕 찝어서 타겟화 시키는게 아니라
타겟의 위치를 좌표화시켜 저장해놓고 해당 좌표가 뷰에 보일때 함수를 실행시켜주는거 아니냐?!
그럼 말이 된다.
리스트가 업데이트될때 타겟의 y값은 저 아래로 밀려나게 된다. 옵저버는 자기가 저장해둔 타겟의 y값을 자동으로 업데이트해주는게 아니라 우리가 다시 옵저버에게 타겟을 봐달라고 말해야한다!
즉, 위의 코드들이 행동패턴?은 다음과 같다.
1. 타겟이 뷰에 100% 포착되면 page가 변하고 page를 감지하던 useEffect를 통해 getList가 실행된다.
2. list가 업데이트 되고 기존 타겟의 위치가 변경된다.
3. loading 또한 업데이트 되며 loading의 변화를 감지하던 useEffect를 통해 옵저버가 새롭게 선언되고 y값이 변한 타겟을 다시 타겟팅한다.
그래서 그렇다구!!!
.......엥 근데 저 코드 보면 observer.observe(ref.current); 가 타겟을 타겟팅해주는거니까 const observer를 useEffect에 넣고 또 선언할 필요는 있는거임?
...네 맞습니다. 실은 위에 저 3번까지 적고나서 흠 이럼 되겟지~ 이러다가.. 근데 저거 빼도 되는거 아니냐? 싶어져서 빼봤는데 잘 작동되었구요..ㅋㅋ 근데 프로파일러로 봤을때 무한스크롤이 실행되고 렌더링 걸린 시간 보니까 5.4ms로 똑같이 뜨길래 별 상관 없는건가 싶기도 하고?
난 useEffect에 많은걸 넣어두기 싫어해서 일단 빼놓고 써야겠다..
레포지토리는 이쪽! 지금은 dev브랜치에만 업데이트 하고있다. 나중에 액션도 연결해서 사이트 하나 파버릴까?! 싶어가지고..
https://github.com/jjubbu/dog_assignment
GitHub - jjubbu/dog_assignment: 도그마스터 과제 레포
도그마스터 과제 레포. Contribute to jjubbu/dog_assignment development by creating an account on GitHub.
github.com
5일이나 받았는데 하루이틀해서 드리는것보다 어느정도 더 꼼꼼히 만지고 과제를 드리는게 주신 분들에 대한 예의라고 생각혀서리.
내일이 1월1일이기도 하고, 주말이기도 해서 일요일까지 더 만지고 월요일 오전 9시 예약메일 발송 맞춰둘 예정.
그럼 20000!
'이것저것 코딩공부' 카테고리의 다른 글
에러사항 및 해결법 기록 (0) | 2022.03.29 |
---|---|
[Typescript] 5252, 타입스크립트.. 한번 해 보자고. (feat.노마드 코더 "Typescript로 블록체인 만들기") (4) | 2022.01.12 |
React quill change image file name (s3) before upload content - 리액트 퀼 s3에 업로드한 이미지로 퀼 컨텐츠의 src 주소를 변경하는 법 (0) | 2021.11.21 |
[React] useState 를 Dictionary 형태로 사용할때 (0) | 2021.10.26 |
React styled-component 형제버튼들 중 하나만 클래스 줄 때 (0) | 2021.10.15 |