Skip to content

Commit b0318cc

Browse files
authored
Merge pull request #110 from oodd-team/feat/OD-151
[OD-151] Home 무한 스크롤 수정
2 parents 34cd968 + db0b07d commit b0318cc

5 files changed

Lines changed: 85 additions & 35 deletions

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
"dayjs": "^1.11.12",
1818
"firebase": "^10.13.0",
1919
"heic2any": "^0.0.4",
20+
"lodash": "^4.17.21",
2021
"react": "^18.3.1",
2122
"react-dom": "^18.3.1",
22-
"react-responsive": "^10.0.0",
23+
"react-responsive": "^10.0.0",
2324
"react-router-dom": "^6.24.1",
2425
"recoil": "^0.7.7",
2526
"recoil-persist": "^5.1.0",
@@ -29,6 +30,7 @@
2930
"swiper": "^11.1.8"
3031
},
3132
"devDependencies": {
33+
"@types/lodash": "^4.17.13",
3234
"@types/node": "^20.14.10",
3335
"@types/react": "^18.3.3",
3436
"@types/react-dom": "^18.3.0",

src/pages/Home/HomeTopBar/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { Button, ButtonContainer, HomeLogo, HomeTopBarContainer } from './styles';
33
import logo from '@assets/default/oodd.svg';
4-
import alarm from '@assets/default/alarm.svg';
4+
import Alarm from '@/components/Icons/Alarm';
55

66
// Home 페이지의 상단 바입니다. 로고와 알림이 있습니다.
77
// TODO: 버튼 클릭 이벤트 처리 필요
@@ -11,7 +11,7 @@ const HomeTopBar: React.FC = () => {
1111
<HomeLogo src={logo} alt="oodd" />
1212
<ButtonContainer>
1313
<Button>
14-
<img src={alarm} alt="알림" />
14+
<Alarm />
1515
</Button>
1616
</ButtonContainer>
1717
</HomeTopBarContainer>

src/pages/Home/OOTD/Feed/index.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@ import {
2121
} from './styles';
2222
import more from '@assets/default/more.svg';
2323
import xBtn from '@assets/default/reject.svg';
24-
import likeBtn from '@assets/default/heart.svg';
25-
import likeFillBtn from '@assets/default/heart-fill.svg';
26-
import commentBtn from '@assets/default/message-white.svg';
2724
import { useNavigate } from 'react-router-dom';
2825
import defaultProfile from '@assets/default/defaultProfile.svg';
2926
import dayjs from 'dayjs';
27+
import Heart from '@/components/Icons/Heart';
28+
import Message from '@/components/Icons/Message';
3029
import { OptionsBottomSheetProps } from '@components/BottomSheet/OptionsBottomSheet/dto';
3130
import OptionsBottomSheet from '@components/BottomSheet/OptionsBottomSheet';
3231
import CommentBottomSheet from '@components/CommentBottomSheet';
@@ -240,14 +239,13 @@ const Feed: React.FC<FeedProps> = ({ feed }) => {
240239
<ReactionWrapper>
241240
<Reaction>
242241
<img className="button" onClick={handleRejectButtonClick} src={xBtn} />
243-
{isLikeClicked ? (
244-
<img className="button" onClick={handleLikeButtonClick} src={likeFillBtn} />
245-
) : (
246-
<img className="button" onClick={handleLikeButtonClick} src={likeBtn} />
247-
)}
242+
<div className="button" onClick={handleLikeButtonClick}>
243+
{/* Heart 컴포넌트의 isFilled 프로퍼티에 isLikeClicked 상태를 전달 */}
244+
<Heart isFilled={isLikeClicked} />
245+
</div>
248246
</Reaction>
249247
<MatchingBtn onClick={handleMatchingButtonClick}>
250-
<img src={commentBtn} />
248+
<Message color="white" />
251249
<StyledText $textTheme={{ style: 'body1-regular' }} color={theme.colors.white}>
252250
매칭 요청
253251
</StyledText>

src/pages/Home/OOTD/index.tsx

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React, { useState, useEffect, useRef, useLayoutEffect } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import debounce from 'lodash/debounce';
23
import { OOTDContainer, FeedContainer } from './styles';
34
import Feed from './Feed/index';
45
import { 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>

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,11 @@
10681068
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
10691069
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
10701070

1071+
"@types/lodash@^4.17.13":
1072+
version "4.17.13"
1073+
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.13.tgz#786e2d67cfd95e32862143abe7463a7f90c300eb"
1074+
integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==
1075+
10711076
"@types/node@>=12.12.47", "@types/node@>=13.7.0":
10721077
version "22.10.2"
10731078
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9"
@@ -1927,6 +1932,11 @@ lodash.merge@^4.6.2:
19271932
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
19281933
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
19291934

1935+
lodash@^4.17.21:
1936+
version "4.17.21"
1937+
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
1938+
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
1939+
19301940
long@^5.0.0:
19311941
version "5.2.3"
19321942
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"

0 commit comments

Comments
 (0)