티스토리 뷰
IntersectionObserver?
대상 요소와 상위 요소, 또는 대상 요소와 최상위 문서의 뷰포트가 서로 교차하는 영역이 달라지는 경우 이를 비동기적으로 감지할 수 있는 수단을 제공하는 인터페이스
주요 개념
target
- 주시할 대상
- 특정 조건(화면에 나타남 or 특정 요소와 만남)을 만족할 경우 행동을 수행하도록 할 수 있음
root
- 주시할 대상이 행동을 수행하기 위해 만나야하는 대상
- null인 경우 화면 전체, 어떠한 실제 HTMLElement일 경우 해당 요소
callback
- 실제로 수행하고자 하는 행동
observer
- 이 모든 상황을 관찰하고 있는 관찰자
- 특정 조건이 만족되었는지를 observer가 감지함
Ref
- React에서 컴포넌트가 아닌 실제 DOM 요소에 접근할 수 있는 객체
- useRef를 통해 선언 후 감지하고자 하는 요소에 전달하면 됨
- 만약 몇 다리 건너야 하는 경우 forwardRef를 사용할 수 있음
사용방법
new IntersectionObserver(callback[, options]);
type IntersectionObserverType = {
callback: (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => void;
options: {
root: React.Ref<HTMLElement>;
threshold:number;
...
}
}
callback
- 대상 요소가 보여지는 비율이 역치를 넘나들 때(즉, 더 많이 보이거나 적게 보일 때 모두) 호출될 함수
entries
- 더 많이 드러나거나 가려져서 지정한 역치를 넘게 된 요소들을 나타내는 IntersectionObserverEntry 객체의 배열
observer
- 콜백을 호출한 IntersectionObserver
options
- 감지기의 동작을 조절할 수 있는 객체
- 지정하지 않으면 문서 뷰포트를 루트로 하고, 여백 없이, 0% 역치(하나의 픽셀이라도 보이면 콜백 호출)로 설정
예시코드
#1 callback 함수를 만든다
- entriies에 observer가 감지하는 target들이 들어있다
- 만약 배열의 길이가 1뿐이라면 entries[0]으로 접근해도 된다
- forEach로 접근해 각각의 entry가 isIntersecting일 때 하고자 하는 행동을 서술하면 된
const callback = useCallback(
(entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (cursorId === null) return;
if (loading) return;
execute();
}
});
},
[execute, loading, cursorId],
);
#2 본격적으로 observer를 활용한다
- 새로운 IntersectionObserver를 만들기 위해 생성자를 호출한다
- 구현한 callback과, observer가 감지로 인정하는 옵셔널한 조건을 명시한다
- react에서 사용하는 관계로, 더이상 사용하지 않을 때 observer를 disconnect 해주는 것도 잊지 않는다
- target은 Ref를 통해 실제 DOM 요소를 가지고 와야한다.
useEffect(() => {
if (!hookEnabled || loading || cursorId === null) return undefined;
const observer = new IntersectionObserver(callback, {
threshold,
root: root?.current,
});
if (target?.current) {
observer.observe(target.current);
}
return () => {
observer.disconnect();
};
}, [target, callback, hookEnabled, root, threshold, cursorId, loading]);
이러한 방식을 활용하면 무한 스크롤 기능을 구현할 수 있다
import { useCallback, useEffect, useState } from 'react';
import { AxiosResponse } from 'axios';
import { useAsync } from './useAsync';
type DefaultInfiniteScrollType = {
cursorId: number | null;
};
type UseInfiniteScrollProps<T> = {
root?: React.RefObject<HTMLElement> | null;
target: React.RefObject<HTMLElement>;
onIntersect: (
cursorId?: number | null,
pageSize?: number,
) => Promise<AxiosResponse<T>>;
threshold?: number;
hookEnabled?: boolean;
};
export default function useInfiniteScroll<T extends DefaultInfiniteScrollType>({
root = null,
target,
onIntersect,
threshold = 1.0,
hookEnabled,
}: UseInfiniteScrollProps<T>) {
const [cursorId, setCursorId] = useState<number | null>();
const [pageSize, setPageSize] = useState<number>(5);
const { execute, loading, error, data } = useAsync<T>({
asyncFunction: async () => {
const response = await onIntersect(cursorId, pageSize);
setCursorId(response.data.cursorId || null);
return response;
},
});
const callback = useCallback(
(entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (cursorId === null) return;
if (loading) return;
execute();
}
});
},
[execute, loading, cursorId],
);
useEffect(() => {
if (!hookEnabled || loading || cursorId === null) return undefined;
const observer = new IntersectionObserver(callback, {
threshold,
root: root?.current,
});
if (target?.current) {
observer.observe(target.current);
}
return () => {
observer.disconnect();
};
}, [target, callback, hookEnabled, root, threshold, cursorId, loading]);
return { loading, error, data, setPageSize };
}
'FE > JS' 카테고리의 다른 글
[TS][React][접근성] non-interactive 한 HTML 요소를 interactive 하게 사용하려면 (1) | 2024.02.09 |
---|---|
[TS][react] 나는 왜 non-null assertion(!)를 사용했을까? (0) | 2024.02.07 |
[React][styled-components] React does not recognize the `backgroundColor` prop on a DOM element. 오류 (0) | 2023.12.14 |
[TypeScript] FormData 에 내용을 담아 post 해보자 (0) | 2023.12.03 |
[React] 참조형과 useState (0) | 2023.11.27 |