1- import React , { useState , useEffect , useRef , useLayoutEffect } from 'react' ;
1+ import React , { useState , useEffect , useRef } from 'react' ;
2+ import debounce from 'lodash/debounce' ;
23import { OOTDContainer , FeedContainer } from './styles' ;
34import Feed from './Feed/index' ;
45import { getPostListApi } from '@apis/post' ;
@@ -13,34 +14,35 @@ const OOTD: React.FC = () => {
1314 const [ modalContent , setModalContent ] = useState ( '알 수 없는 오류입니다.\n관리자에게 문의해 주세요.' ) ;
1415 const [ isStatusModalOpen , setIsStatusModalOpen ] = useState ( false ) ;
1516
16- const [ reachedEnd , setReachedEnd ] = useState ( false ) ;
17+ // API 요청 중인지 확인하는 변수 (렌더링 없이 상태 관리)
1718 const isFetchingRef = useRef ( false ) ;
19+ // 모든 데이터를 불러왔는지 확인하는 변수
20+ const isReachedEndRef = useRef ( false ) ;
21+
22+ // 현재 페이지 번호를 참조하는 변수, 리렌더링 없이 값만 업데이트하기 위해 상태가 아닌 useRef 사용
1823 const feedPageRef = useRef ( 1 ) ;
24+
25+ // 세션 스토리지에서 이전 스크롤 위치를 가져와 초기화
1926 const savedScrollPosition = sessionStorage . getItem ( 'scrollPosition' ) ;
2027 const scrollPositionRef = useRef ( Number ( savedScrollPosition ) || 0 ) ;
2128
22- // 스크롤 이벤트 핸들러 추가
23- const handleScroll = ( ) => {
24- // 모든 데이터를 불러왔거나 아직 렌더링이 다 안 된 경우 반환
25- if ( reachedEnd || isFetchingRef . current ) return ;
26-
27- if ( window . innerHeight + document . documentElement . scrollTop >= document . body . scrollHeight - window . innerHeight ) {
28- isFetchingRef . current = true ;
29- scrollPositionRef . current = window . scrollY ; // 현재 스크롤 위치 저장
30- getPostList ( ) ;
31- }
32- } ;
29+ // IntersectionObserver 인스턴스를 참조하는 변수
30+ const observerRef = useRef < IntersectionObserver | null > ( null ) ;
31+ // 더 많은 데이터를 로드할 때 관찰할 마지막 요소의 DOM을 참조
32+ const loadMoreRef = useRef < HTMLDivElement | null > ( null ) ;
3333
34- // 전체 게시글(피드) 조회 api
34+ // 전체 게시글(피드) 조회 API
3535 const getPostList = async ( ) => {
36- if ( reachedEnd ) return ;
36+ // 모든 데이터를 불러왔거나 요청 중이라면 함수 실행 중단
37+ if ( isReachedEndRef . current || isFetchingRef . current ) return ;
3738
39+ isFetchingRef . current = true ; // 요청 중임을 표시
3840 try {
3941 const response = await getPostListApi ( feedPageRef . current , 20 ) ;
4042
4143 if ( response . isSuccess ) {
4244 if ( response . data . post . length === 0 ) {
43- setReachedEnd ( true ) ;
45+ isReachedEndRef . current = true ; // 더 이상 불러올 데이터가 없음을 표시
4446 } else {
4547 setFeeds ( ( prevFeeds ) => [ ...prevFeeds , ...response . data . post ] ) ;
4648 feedPageRef . current += 1 ;
@@ -50,22 +52,58 @@ const OOTD: React.FC = () => {
5052 const errorMessage = handleError ( error ) ;
5153 setModalContent ( errorMessage ) ;
5254 setIsStatusModalOpen ( true ) ;
55+ } finally {
56+ isFetchingRef . current = false ;
5357 }
5458 } ;
5559
5660 useEffect ( ( ) => {
57- getPostList ( ) ;
58- window . addEventListener ( 'scroll' , handleScroll ) ;
61+ if ( isReachedEndRef . current && observerRef . current && loadMoreRef . current ) {
62+ observerRef . current . unobserve ( loadMoreRef . current ) ; // 데이터의 끝에 다다르면 옵저버 해제 (더이상 피드가 없으면)
63+ return ;
64+ }
65+
66+ // Intersection Observer 생성
67+ observerRef . current = new IntersectionObserver (
68+ debounce ( ( entries ) => {
69+ const target = entries [ 0 ] ;
70+ console . log ( 'Intersection Observer:' , target . isIntersecting ) ;
71+ if ( target . isIntersecting && ! isFetchingRef . current && ! isReachedEndRef . current ) {
72+ // 요소가 화면에 보이고 있고, 요청 중이 아니며 끝에 도달하지 않았다면 API 호출
73+ getPostList ( ) ;
74+ }
75+ } , 300 ) , // 디바운스 적용, 마지막 스크롤 이후 300ms 동안 동작이 없으면 이벤트 호출
76+ {
77+ root : null ,
78+ rootMargin : '100px' , // 요소가 보이기 100px 전에 미리 데이터 로드
79+ threshold : 0 , // 요소가 아주 조금이라도 보이면 트리거
80+ } ,
81+ ) ;
5982
83+ // 옵저버를 마지막 요소에 연결
84+ if ( loadMoreRef . current ) {
85+ observerRef . current . observe ( loadMoreRef . current ) ;
86+ }
6087 return ( ) => {
61- window . removeEventListener ( 'scroll' , handleScroll ) ;
88+ // 컴포넌트 언마운트 시 옵저버 해제
89+ if ( observerRef . current && loadMoreRef . current ) {
90+ observerRef . current . unobserve ( loadMoreRef . current ) ;
91+ }
6292 } ;
6393 } , [ ] ) ;
6494
65- useLayoutEffect ( ( ) => {
66- window . scrollTo ( 0 , scrollPositionRef . current ) ; // 저장된 스크롤 위치로 이동
67- isFetchingRef . current = false ;
68- } , [ feeds ] ) ; // feeds가 변경될 때 실행
95+ useEffect ( ( ) => {
96+ // 첫 로드 시 API 호출
97+ getPostList ( ) ;
98+
99+ // 세션 저장된 이전 스크롤 위치 복원
100+ window . scrollTo ( 0 , scrollPositionRef . current ) ;
101+
102+ return ( ) => {
103+ // 컴포넌트 언마운트 시 현재 스크롤 위치를 세션 스토리지에 저장
104+ sessionStorage . setItem ( 'scrollPosition' , String ( window . scrollY ) ) ;
105+ } ;
106+ } , [ ] ) ;
69107
70108 const statusModalProps : ModalProps = {
71109 content : modalContent ,
@@ -82,6 +120,8 @@ const OOTD: React.FC = () => {
82120 < Feed feed = { feed } />
83121 </ div >
84122 ) ) }
123+ { /* Intersection Observer가 감지할 마지막 요소 */ }
124+ < div ref = { loadMoreRef } style = { { height : '1px' , backgroundColor : 'transparent' } } />
85125 </ FeedContainer >
86126 { isStatusModalOpen && < Modal { ...statusModalProps } /> }
87127 </ OOTDContainer >
0 commit comments