Skip to content

Commit e0ce7d9

Browse files
authored
Merge pull request #77 from 3rdflr/fix/TRI-84-activities-image
Fix TRI-84: 성능 향상을 위한 이미지 및 컴포넌트 로딩 최적화
2 parents 1e98859 + 2b22381 commit e0ce7d9

25 files changed

+503
-196
lines changed

next.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** @type {import('next').NextConfig} */
12
const nextConfig = {
23
images: {
34
unoptimized: true,

package-lock.json

Lines changed: 186 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"scripts": {
66
"dev": "next dev",
77
"build": "next build",
8+
"analyze": "npx @next/bundle-analyzer .next/static/chunks/",
89
"start": "next start",
910
"lint": "next lint",
1011
"prepare": "husky",
@@ -74,6 +75,7 @@
7475
},
7576
"devDependencies": {
7677
"@eslint/eslintrc": "^3",
78+
"@next/bundle-analyzer": "^15.5.3",
7779
"@storybook/addon-essentials": "7.6.20",
7880
"@storybook/addon-interactions": "7.6.20",
7981
"@storybook/addon-links": "7.6.20",

src/app/activities/[activityId]/ActivityClientPage.tsx

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client';
22
import { useState, useEffect, useMemo } from 'react';
3-
import { useSuspenseQuery } from '@tanstack/react-query';
3+
import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query';
44
import { getActivityDetail } from '@/app/api/activities';
5+
import type { ActivityDetail } from '@/types/activities.type';
56
import { useRecentViewedStore } from '@/store/recentlyWatched';
67
import ActivityImageViewer from '@/components/pages/activities/ActivityImageViewer';
78
import ActivityInfo from '@/components/pages/activities/ActivityInfo';
@@ -12,6 +13,7 @@ import Marker from '@/components/common/naverMaps/Marker';
1213
import ImageMarker from '@/components/common/naverMaps/ImageMarker';
1314
import { activityQueryKeys } from './queryKeys';
1415
import { useUserStore } from '@/store/userStore';
16+
import { useIntersectionObserver } from '@/hooks/useIntersectionObserver';
1517

1618
/**
1719
* ActivityClient 컴포넌트
@@ -25,7 +27,13 @@ interface ActivityClientProps {
2527

2628
export default function ActivityClient({ activityId, blurImage }: ActivityClientProps) {
2729
const [isOwner, setIsOwner] = useState<boolean>(false);
28-
const user = useUserStore((state) => state.user);
30+
const { user } = useUserStore();
31+
const queryClient = useQueryClient();
32+
33+
const [mapRef, isMapVisible] = useIntersectionObserver({
34+
rootMargin: '100px',
35+
triggerOnce: true,
36+
});
2937

3038
// 1. 정적 데이터 (이미지, 주소, 제목, 설명) - 긴 캐시
3139
const { data: staticInfo } = useSuspenseQuery({
@@ -47,11 +55,31 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient
4755
gcTime: 60 * 60 * 1000, // 1시간 메모리 보관
4856
});
4957

50-
// 2. 동적 데이터 (가격, 스케줄, 평점) - 짧은 캐시
58+
// 2. 동적 데이터 (가격, 스케줄, 평점) - 짧은 캐시, 재사용 최적화
5159
const { data: dynamicInfo } = useSuspenseQuery({
5260
queryKey: [...activityQueryKeys.detail(activityId), 'dynamic'],
53-
queryFn: () => getActivityDetail(Number(activityId)),
54-
select: (data) => ({
61+
queryFn: (): Promise<ActivityDetail> => {
62+
// 캐시된 데이터가 있으면 재사용, 없으면 새로 호출
63+
const cachedData = queryClient.getQueryData<ActivityDetail>([
64+
...activityQueryKeys.detail(activityId),
65+
'static',
66+
]);
67+
const cachedState = queryClient.getQueryState([
68+
...activityQueryKeys.detail(activityId),
69+
'static',
70+
]);
71+
72+
// static 캐시가 fresh하면 재사용
73+
if (
74+
cachedData &&
75+
cachedState?.dataUpdatedAt &&
76+
Date.now() - cachedState.dataUpdatedAt < 2 * 60 * 1000
77+
) {
78+
return Promise.resolve(cachedData); // 캐시 재사용
79+
}
80+
return getActivityDetail(Number(activityId)); // 새 호출
81+
},
82+
select: (data: ActivityDetail) => ({
5583
price: data.price,
5684
schedules: data.schedules,
5785
rating: data.rating,
@@ -69,11 +97,10 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient
6997
const addViewed = useRecentViewedStore((s) => s.addViewed);
7098

7199
useEffect(() => {
72-
if (activity) {
100+
if (activity?.id) {
73101
addViewed(activity);
74-
console.log('👀 최근 본 목록에 추가됨', activity.title);
75102
}
76-
}, [activity, addViewed]);
103+
}, [activity, addViewed]); // eslint 경고 해결을 위해 원복하되, 조건문 최적화
77104

78105
useEffect(() => {
79106
if (user?.id === activity.userId) {
@@ -107,14 +134,18 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient
107134
</section>
108135
<hr className='border-gray-100' />
109136
{/* 주소 섹션 */}
110-
<section className='flex flex-col gap-2'>
137+
<section ref={mapRef} className='flex flex-col gap-2'>
111138
<h2 className='text-lg font-semibold'>오시는 길</h2>
112139
<p className='text-sm text-gray-600'>{activity.address}</p>
113-
<NaverMap address={activity.address} height='256px' zoom={12}>
114-
<Marker address={activity.address} id='image-marker'>
115-
<ImageMarker src={activity.bannerImageUrl} alt='주소 마커' size={40} />
116-
</Marker>
117-
</NaverMap>
140+
{isMapVisible ? (
141+
<NaverMap address={activity.address} height='256px' zoom={12}>
142+
<Marker address={activity.address} id='image-marker'>
143+
<ImageMarker src={activity.bannerImageUrl} alt='주소 마커' size={40} />
144+
</Marker>
145+
</NaverMap>
146+
) : (
147+
<div className='h-64 bg-gray-100 rounded-lg animate-pulse flex items-center justify-center'></div>
148+
)}
118149
</section>
119150
<hr className='border-gray-100' />
120151
{/* 후기 섹션 */}

0 commit comments

Comments
 (0)