SSR 구현 + 트러블슈팅 401 인증(쿠키) 오류

박기범·2025년 2월 5일
0

🚀 Next.js 서버 컴포넌트에서 Axios 요청 시 401 오류 해결하기

위의 페이지는 사용자가 인증했던 사진을 보여주는 페이지인데
처음 구현했을때 CSR로 구현하였음.
따라서 이미지가 5개 밖에 없지만 이후 이미지가 많아지면 로딩이 느려질 것으로 예상
그래서 Server Side Rendering을 하기로 하였음.

전 프로젝트에서도 SSR을 다뤄본 적이 있어서 로직은 쉽게 짤 수 있었다.

Tanstack-Query의 prefetch, dehydrate, HydrationBoundary를 사용하였다.

  • prefetch
    서버에서 데이터를 미리 받아오기 때문에, 클라이언트에서는 즉시 데이터를 사용할 수 있어 페이지가 빠르게 렌더링.
  • dehydrate
    서버에서 미리 가져온 데이터를 직렬화하여 클라이언트에서 사용할 수 있도록 변환.
  • HydrationBoundary
    클라이언트에서 React Query 캐시를 복원하여 SSR로 가져온 데이터를 재사용.
    클라이언트에서 추가적인 API 호출 없이 즉시 상태를 사용 가능.

로직 정리

  1. QueryClient를 생성하고, prefetchQuery를 이용해 API 요청을 미리 수행.
  2. 서버에서 가져온 데이터를 dehydrate(queryClient)를 통해 직렬화하여 클라이언트로 전달.
  3. 클라이언트에서 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>
  );
}

SSR 적용전 (LCP 점수가 매우 좋지않았음.)




신기하게도 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="인증한 이미지"
 />

SSR 적용후 + 이미지 최적화 (LCP 점수가 약 21배 개선 )

...
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 성능 개선에 들어갈 예정이다.

profile
프론트엔드 개발공부를 하고있습니다.

0개의 댓글

관련 채용 정보