위의 페이지는 사용자가 인증했던 사진을 보여주는 페이지인데
처음 구현했을때 CSR로 구현하였음.
따라서 이미지가 5개 밖에 없지만 이후 이미지가 많아지면 로딩이 느려질 것으로 예상
그래서 Server Side Rendering을 하기로 하였음.
전 프로젝트에서도 SSR을 다뤄본 적이 있어서 로직은 쉽게 짤 수 있었다.
Tanstack-Query의 prefetch, dehydrate, HydrationBoundary를 사용하였다.
- prefetch
서버에서 데이터를 미리 받아오기 때문에, 클라이언트에서는 즉시 데이터를 사용할 수 있어 페이지가 빠르게 렌더링.- dehydrate
서버에서 미리 가져온 데이터를 직렬화하여 클라이언트에서 사용할 수 있도록 변환.- HydrationBoundary
클라이언트에서 React Query 캐시를 복원하여 SSR로 가져온 데이터를 재사용.
클라이언트에서 추가적인 API 호출 없이 즉시 상태를 사용 가능.
로직 정리
- QueryClient를 생성하고, prefetchQuery를 이용해 API 요청을 미리 수행.
- 서버에서 가져온 데이터를 dehydrate(queryClient)를 통해 직렬화하여 클라이언트로 전달.
- 클라이언트에서 HydrationBoundary를 통해 데이터를 재사용, 추가적인 API 호출 없이 빠르게 렌더링.
따라서 아래와 같이 구현하였다. 하지만 SSR이 적용이 안되었음.
왜 자꾸 안되지 고민을 한참하고 미리 SSR을 시도해본 팀원의 말을 들어봤을 때
아마 쿠키 불러오는 문제일꺼다(?!)라고 말했다.
...서버 컴포넌트
export default async function userProfile({
params,
}: {
params: Promise<UserProfileProps>;
}) {
const { userId } = await params;
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: [QUERY_KEYS.USER_PROFILE, userId],
queryFn: () =>
GET<UserProfileResponse>(
API_ENDPOINTS.AUTH.USER_PROFILE(Number(userId)),
),
});
return (
<PageContainer>
<HydrationBoundary state={dehydratedState}>
<UserProfileHeader userId={userId} />
<UserProfileContent userId={userId} />
</HydrationBoundary>
</PageContainer>
);
}
신기하게도 FCP 는 잘나왔다. 하지만 LCP의 개선이 필요했다.
이미지 최적화와 동시에 LCP 요소를 서버에서 미리 렌더링을 할 수 있는 SSR 방식을 적용하기로 하였음.
또한 문제는 무엇이였냐?
기존 prefetch의 함수는 axiosInstance를 사용하였는데
쿠키를 가져오는 방식이 클라이언트 컴포넌트에서는 정상 작동을 하지만 서버 컴포넌트에서는 사용할 수 없기 때문이였다. 비교적 간단한 문제였다.import axios from 'axios'; const apiUrl = process.env.NEXT_PUBLIC_API_URL; const axiosInstance = axios.create({ baseURL: apiUrl, headers: { 'Content-Type': 'application/json', Accept: '*/*', }, }); axiosInstance.interceptors.request.use( (config) => { const getCookie = (name: string) => { const matches = document.cookie.match( new RegExp(`(^|; )${name}=([^;]*)`) ); return matches ? decodeURIComponent(matches[2]) : null; }; const token = getCookie('token'); // 'token' 쿠키 값을 가져옴 if (token) { config.headers['token'] = token; // 헤더에 추가 } return config; }, (error) => { return Promise.reject(error); } ); export default axiosInstance;
따라서 아래의 블로그에서 보았던
https://2yh-develop4jeon.tistory.com/87
cookies함수 : next.js 13부터 제공되는 함수, 서버 컴포넌트에서 쿠키를 읽고 쓸 수 있도록 해주는 함수를 사용하기로 했다.
- 결국 서버 컴포넌트에서 cookies함수를 사용하여 토큰을 불러옴.
- axiosInstance를 사용하지 않고, API 요청 함수에서 직접 axios.get()을 호출.
import axios from 'axios'; import { API_ENDPOINTS } from '@/constants/ApiEndpoints'; const apiUrl = process.env.NEXT_PUBLIC_API_URL; export const getUserProfile = async (userId: number, token: string) => { try { const response = await axios.get( apiUrl + API_ENDPOINTS.AUTH.USER_PROFILE(userId), { headers: { token: token, 'Content-Type': 'application/json', Accept: '*/*', }, }, ); return response.data; } catch (error) { console.error('❌ API 요청 실패:', error); throw new Error('사용자 프로필을 가져오는 중 문제가 발생했습니다.'); } };
또한 LCP 최적화를 위해
Next/Image 컴포넌트에서
- 기존 Webp형식에서 압축률이 더 좋은 avif 이미지 형식로 변경 (리소스 크기 감소)
- 반응형 이미지 최적화 (sizes 사용)
- LCP 개선을 위한 우선 로드(priority)
<Image priority fill sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" style={{ objectFit: 'cover' }} className="rounded-4" src={complete.completePic} alt="인증한 이미지" />
...
export default async function userProfile({
params,
}: {
params: Promise<UserProfileProps>;
}) {
const { userId } = await params;
const cookieStore = await cookies();
const token = cookieStore.get('token')?.value || '';
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: [QUERY_KEYS.USER_PROFILE, Number(userId)],
queryFn: () => getUserProfile(Number(userId), token),
});
return (
<PageContainer>
<HydrationBoundary state={dehydrate(queryClient)}>
<UserProfileHeader userId={userId} />
<UserProfileContent userId={userId} />
</HydrationBoundary>
</PageContainer>
);
}
마지막으로 TBT 시간이 비정상적이게 길어 곧 바로 TBT 성능 개선에 들어갈 예정이다.