https://simian114.gitbook.io/blog/undefined/react/intersectionobserverapi

 

IntersectionObserverAPI로 무한스크롤 구현 - 공부방

자, 이렇게 관찰자, 관찰 대상, 조건, 콜백함수 를 다 만들었으니 끝난걸까? 아니다. 지금 이대로 실행하면 절대 원하는 결과를 얻지 못한다. 왜? 관찰 대상 은 새로운 데이터를 가져올 때 마다 변

simian114.gitbook.io

위 게시물의 내용을 보고 이해한 내용을 담았다.

 

필요한 개념:

 - useState로 반환된 setter 함수랑 useref랑 연동해서 쓰는 법

 - 리액트 state를 스냅샷으로 관리하는 개념, 이를 통해서 useEffect에서 언마운트 될 때 observe 대상 변경하는 것

 

import { useEffect, useRef, useState } from 'react';
import 'intersection-observer';

export const useIntersectionObserver = (callback) => {
  const [observationTarget, setObservationTarget] = useState(null);
  const observer = useRef(
    new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) return;
        callback();
      })},
      { threshold: 1 }
    )
  );

  useEffect(() => {
    const currentObserver = observer.current;
    if (observationTarget) {
      currentObserver.observe(observationTarget);
    }
    return () => {
      if (observationTarget) {
        currentObserver.unobserve(observationTarget);
      }
    };
  }, [observationTarget]);

  return setObservationTarget;
};

 

커스텀 훅을 만드는 코드이다.

  1. useState를 통해 observationTarget과 setObservationTarget을 생성한다.
  2. observer는 리랜더링에도 새로 생성되지 않도록, 성능향상을 위해 useRef로 만들어준다. 교차하고 있다면 콜백함수를 실행시켜준다.
  3. observationTarget이 변경될 때마다 useEffect를 통해 새로운 타겟을 구독한다.
  4. useEffect가 언마운트 될 때, 기존의 구독을 풀어준다.

사용은 이런식으로 한다.

const setObservationTarget = useIntersectionObserver(fetchMoreComments);

function CardList() {
  ...
    return (
    <StyledCardListContainer>
      {comments.map((comment) => (
        <Card key={comment.id} comment={comment} />
      ))}
      {isLoading && <div>Loading...</div>}
      {!isLoading && <div ref={setObservationTarget}></div>}
    </StyledCardListContainer>
  );

 

  1. setObservationTarget이 리턴되었다.
  2. ref에 set함수를 넣어준다, observationTarget은 해당 ref를 참조한다.
  3. 해당 div에 해당하는 값이 setObservationTarget()안에 들어가서 훅이 실행된다.
  4. ObsercationTarget의 값이 변하므로, useEffect도 실행된다.
  5. currentTarget에 해당 div가 들어오면서,observer가 observe 할 수 있다.

일단, intersection observer는 왜 사용할까?

  1. 비동기적으로 콜백을 실행한다. 메인 thread에 영향을 주지 않고 callback을 실행할 수 있도록 한다.
  2. 매번 layout을 새롭게 그려서 render tree를 새로 만들지 않고 callback을 실행한다. -> 일반적인 스크롤 이벤트 요소들의 교차를 감지하는 경우, 스크롤 이벤트가 발생할 때마다 모든 요소들의 위치를 계산하여 레이아웃을 다시 만들어야 하고, 그에 따라 렌더 트리도 새롭게 만들어야 한다. -> 일반적인 스크롤 이벤트로 교차를 확인하면 라우저의 성능을 저하시킬 수 있다.

사용법

let observer = new IntersectionObserver(callback, options);

이렇게 intersection observer (겹치는지 감시하는 놈)을 만든다고 가정하자.

 

options에는 이런 옵션 객체가 들어간다.

let options = {
  root: document.querySelector("#scrollArea"),
  rootMargin: "0px",
  threshold: 1.0,
};
  • root: 대상의 가시성을 확인하기 위한 뷰포트로 사용되는 요소이다. 대상의 조상이어야만 하며, default로는 브라우저 뷰포트이다.
  • rootMargin: 루트 주변의 마진값을 뜻하며, 겹쳤는지 계산하기 전에 root element의 bounding box를 늘리거나 줄이기 위해 사용한다.
  • threshold: target의 가시성이 얼마나 될 때 observer의 콜백을 실행할지 지정하는 것이다.
let thresholdSets = [
    [],
    [0.5],
    [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
    [0, 0.25, 0.5, 0.75, 1.0],
  ];
for (let i = 0; i <= 1.0; i += 0.01) {
  thresholdSets[0].push(i);
}

이런식으로 thresholdSets 배열을 통해 각각 저 값을 threshold로 가지는 옵저버 4개를 만든다고 가정하자.

thresholdSets[3]을 통해 만들어진 옵저버의 경우, intersectionRatio가 0, 0.25, 0.5, 0.75, 1일 때마다 콜백이 실행되는데,

 

let target = document.querySelector("#listItem");
observer.observe(target);

위에서 생성된 observer 객체에 target들을 구독하는 방식이다.

여태까지의 내용은 그냥 훑어봐도 이해가 쉽게 된다.

 

문제

mdn의 intersection observer api를 읽고 있는데, 이 부분이 이해가 가지 않았다..

const intersectionCallback = (entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      let elem = entry.target;

      if (entry.intersectionRatio >= 0.75) {
        intersectionCounter++;
      }
    }
  });
};

The callback receives a list of [IntersectionObserverEntry](<https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry>) objects and the observer

문서에는 이렇게만 적혀있었다.

그래서 entries가 뭔지 알아봐야했다.

 

entries는 구독 중인 동시에, 교차하고 있는 모든 요소들을 담은 배열이다.

자세한 내용은 다른 글로 분리했다..

https://hwangjiwon1.tistory.com/71 

 

intersection observer 진짜 마지막으로 이해하기

Edit fiddle - JSFiddle - Code Playground Edit fiddle - JSFiddle - Code Playground jsfiddle.net intersection observer intersection observer @Infinite scroll - 커스텀 훅 (어려움) www.notion.so 위의 게시물에서 entries는 구독하고 있는 3개

hwangjiwon1.tistory.com

 

그 예시로, https://jsfiddle.net/jiwon22/sm2c1kuy/1/ 여기 들어가서 확인해볼 수 있다.

결론

챗 지피티가 내 시간을 엄청 잡아먹었다.

 

내가 이해하고 있던 내용이 맞았는데, 챗 지피티가 아니라고 해서 mdn 문서와 그 playground 들어가서 콘솔로그를 얼마나 찍었는지…

역시 콘솔로그는 강력하다.

다음에 이런 문제가 생겼을 때 어떻게 접근해야 빨리 궁금증을 해결할 수 있을지도 알았다.

이를 바탕으로 intersection observer + infinite scroll을 커스텀 훅으로 변경하는 부분도 작성할 것이다.

백엔드에서의 무한 대댓글 구현을 적다가, 백엔드에서 만약 댓글들의 depth 처리를 안 해주면 어떻게 해야할까 싶었다.

 

카카오 테크 캠퍼스에서 내가 처리했던 데이터처럼, 배열 안의 배열 안의 배열 안의 … 이런식으로 댓글 정보를 준다면 어떻게 해야할까?

 

고민하다가 내가 생각해낸 순서는 이렇다.

DFS - 실패 ㅋㅋ

재귀적으로 대댓글이 있다면, depth를 1씩 더하면서 코멘트를 새로 생성하는 식의 코드를 생각해봤다.

const handleDeepComment = (comments, depth) => {
  comments.map((comment) => {
    if(comment.comments.length > 0) {
      return handleDeepComment(comment, depth + 1);
    }
    return <Comment depth={depth} comment={comment} />;
  })
}

이런 댓글을 가정해보자.

  • 1
    • 2
      • 3
      • 4
    • 5

첫 번째 댓글에서 바로 재귀 함수가 실행된다.

두 번째 댓글에서도 바로 재귀 함수가 실행된다.

3, 4가 먼저 반환되고, 2, 5, 1 순으로 반환된다.

DFS + 배열 - 성공

const results = [];
const handleDeepComment = (comments, depth) => {

  comments.forEach((comment) => {
    // 원본 댓글을 먼저 추가
    results.push({ comment.content, depth });

		// 하위에 대댓글이 있으면 재귀함수 실행
    if (comment.comments.length > 0) {
      handleDeepComment(comment.comments, depth + 1);
    }
  });

};

// 함수 호출은 handleDeepComment(fullCommentData, 0) 이런식으로..

중간에 push를 넣어주면 해결될 것 같았다.

위의 예시를 다시 가져와보자.

  • 1
    • 2
      • 3
      • 4
    • 5

1이 depth 0과 함께 들어간다. → 2, 5를 대댓글로 갖고 있으므로, 재귀함수가 실행된다.

2가 depth 1과 함께 들어간다. → 3, 4를 대댓글로 갖고 있으므로, 재귀함수가 실행된다.

3가 depth 2와 함께 들어간다. → 대댓글이 없으므로 끝난다.

4가 depth 2와 함께 들어간다. → 대댓글이 없으므로 끝난다.

5가 depth 1과 함께 들어간다. → 대댓글이 없으므로 종료.

 

이렇게 results에 댓글이 depth와 함께 순서대로 잘 들어가 있으므로, results의 정보를 차례대로 출력하기만 하면 된다.

 

부모관계까지 제대로 해주고 싶다면?????

forEach문을 실행시키는 comments 녀석을 부모로 같이 넣어주면 된다.

const results = [];
const handleDeepComment = (comments, depth) => {

  comments.forEach((comment) => {

    results.push({ comment.content, depth, comments.parentId });

		// 하위에 대댓글이 있으면 재귀함수 실행
    if (comment.comments.length > 0) {
      handleDeepComment(comment.comments, depth + 1);
    }
  });

};

 

이제 백엔드에서 전처리되지 않은 데이터를 줄까봐 무서워하지 않아도 된다!!

카카오 테크 캠퍼스에서 트래픽이 몰리는 경우를 대비하기 위한 ‘레디스’라는 개념을 봤다. 몰라서 찾아보았는데, 엄청난 것이었다.

해피쿠 블로그 - [Redis] 인-메모리 데이터베이스 Redis

 

해피쿠 블로그 - [Redis] 인-메모리 데이터베이스 Redis

누구나 손쉽게 운영하는 블로그!

www.happykoo.net

이 블로그의 내용이 첫 이해에 정~~말 많이 도움이 됐다.

 

Redis는 DBMS(DataBase Management System)인데, 메모리 기반 DBMS이다.

 

보통 Session Management, Look aside Cache, Write Bach 목적으로 사용된다고 하는데, 해당 설명은 블로그에 너무 쉽고 잘 설명되어 있어서 다루지 않는다. 나중에 직접 구현할 때 정리해 두면 좋을 것 같긴 하다.


검색하다 보니, O(N) 실행속도의 명령어들을 조심해서 사용하라는 내용을 지속해서 봤다. 따라서 이 부분에 대해 찾아보고 정리할 것이다.

 

알고리즘 문제를 풀어보면서 O(N) 보다 속도가 빠른 것은 O(log n)과 O(1) 밖에 없었다. 전자의 가장 흔한 예시는 이분탐색이 있겠고, 후자의 가장 흔한 예시는 push, pop 정도?

아무튼 내 상식상 O(N)이 걸리는 작업은 결국 O(N)에 걸쳐서 해결해야 하는 것 아닌가 싶었다.

SCAN

O(N)의 속도를 가진 KEYS는 모든 키를 검색하는 명령어이다. 원하는 패턴과 일치하는 모든 키를 찾을 때 사용이 되는데, 결국 하나씩 전부 확인하며 찾아야 해서, O(N)의 속도를 가질 수밖에 없다.

 

Redis는 Single Thread로 동시에 딱!!! 하나의 명령만 처리할 수 있기에, KEYS와 같이 긴 시간이 필요한 명령을 수행하다보면 터진다. 뒤의 명령이 밀리도록 하면 안 된다.

대표적인 O(N) 명령은 KEYS, FLUSHALL, FLUSHDB, Delete Collections, Get All Collections로,

아이템의 N이 적다면, 당연히 문제가 되지 않는다. 근데 아이템이 많다면 위에서 말한 문제가 터지기 시작한다.

 

SCAN 명령은 KEYS와 비슷한데, 짧은 명령을 많이 보내는 것이다.

명령과 명령 사이에 다른 O(1) 짜리 짧은 명령어 수천, 수만개씩을 처리할 수 있기 때문에 터지지 않도록 방지할 수 있다.

 

Collection의 모든 item을 가져와야 하는 상황을 대비하기 위해서는 애초에 Collection을 작은 여러 개의 Collections로 나눠서 저장하면 좋다.

그렇게 하나씩 가져오면 좋다.

 

redis KEYS 공식문서

 

KEYS

Returns all key names that match a pattern.

redis.io

KEYS vs SCAN stackoverflow

 

SCAN vs KEYS performance in Redis

A number of sources, including the official Redis documentation, note that using the KEYS command is a bad idea in production environments due to possible blocking. If the approximate size of the d...

stackoverflow.com

+ Recent posts