Next.js 13의 SSR 사용 ( Fetch API )
포스트
취소

Next.js 13의 SSR 사용 ( Fetch API )

해당 포스트는 blegram이라는 솔로 프로젝트를 만들면서 사용했던 Next.js 13SSRFetch API에 대한 내용을 기록하는 포스트입니다.

아직 Next.js 13에 대해 공부가 부족한 부분이 많아서 잘못된 내용이 포함되어있을 확률이 높습니다…🥲
지금 기록해두지 않으면 나중에 지금의 경험을 잊을 것 같아서 기록하는 용도로 작성했습니다.

🔥 사용

0️⃣ “use client”

Next.js 13에서는 기본적으로 컴포넌트를 SSR을 하고 "use client"을 최상위에 작성한 컴포넌트는 CSR을 합니다.

CSR을 적용해야하는 컴포넌트는 클라이언트에서만 사용할 수 있는 기능을 사용하는 컴포넌트입니다.
대표적으로 use를 붙이는 훅을 사용하는 컴포넌트입니다.

처음에는 이 부분을 몰라서 모든 페이지 컴포넌트에 "use client"를 붙였습니다.
그렇게 하면 metadata를 사용할 수 없기 때문에 어떻게 해야하는지 몰라서 일단 넘어갔다가 이후에 모두 수정했습니다.

페이지 컴포넌트에서 CSR이 필요한 컴포넌트들은 또 따로 컴포넌트로 제작하고 상단에 "use client"를 붙이는 방식으로 작성해야 합니다.

추가적으로 "use client"가 붙은 컴포넌트의 하위 컴포넌트는 모두 CSR이 됩니다. 즉, "use client"가 있는 것과 같이 처리됩니다.

1️⃣ Fetch API와 Axios

13이전에는 <Head>getStaticProps(), getServerSideProps()와 같은 방법으로 SSR을 적용했는데 13부터는 페이지 컴포넌트 ( "use cleint"를 붙이지 않은 컴포넌트 ) 자체에 Fetch API를 사용하면 됩니다.
no-cache, force-cache, next: { validate: 60 }과 같은 옵션 값을 이용해서 이전에 사용했던 메서드 각자의 특성을 적용할 수 있습니다.

  • 알아야 할 것
    1. 구체적인 URL을 지정해줘야 함
    2. axios는 사용하기 불편함 ( fetch처럼 타입이 적용되지 않음 )
    3. 기본적으로 같은 요청이라면 fetch()를 여러 번 사용해도 캐싱된 데이터를 사용함 ( 기본 값 force-cache )
  • /src/app/page.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** 2023/05/07 - 게시글들 가져오기 요청 - by 1-blue */
const fetchPosts: ApiFetchPostsHandler = async ({ take, lastIdx = -1 }) =>
  fetch(process.env.BASE_URL + `/api/posts?take=${take}&lastIdx=${lastIdx}`, {
    cache: "no-cache",
  }).then((res) => res.json());

interface Props {
  searchParams: { postIdx: string | undefined };
}

/** 2023/04/30 - 메타데이터 - by 1-blue */
export const generateMetadata = async ({
  searchParams: { postIdx },
}: Props): Promise<Metadata> => {
  // 서버 사이드에서 실행됨
  const data = await apiServiceSSR.fetchPosts({
    take: 10,
    lastIdx: Number(postIdx) || -1,
  });

  return getMetadata({
    title: "메인",
    description: data?.posts?.[0].content || "게시글이 존재하지 않습니다.",
    images: data.posts?.[0].photos[0]
      ? [combinePhotoURL(data.posts[0].photos[0])]
      : undefined,
  });
};

/** 2023/03/24 - 홈 페이지 - by 1-blue ( 2023/04/09 ) */
const HomePage = async ({ searchParams: { postIdx } }: Props) => {
  // 서버 사이드에서 실행됨
  const initialData = await apiServiceSSR.fetchPosts({
    take: 10,
    lastIdx: Number(postIdx) || -1,
  });

  // 서버 사이드에서 패치한 데이터를 클라이언트 사이드의 컴포넌트로 내려줌
  return <Post initialData={initialData} />;
};

export default HomePage;

2️⃣ metadata

메타데이터에 대한 내용이 너무 많아서 작성하면 좋을 것 같은 부분만 작성했습니다.
매번 페이지 컴포넌트에 작성하기에는 반복이 많아서 모듈로 분리했습니다.
( styled-components를 적용하다보니 layout.tsx가 클라이언트 사이드 컴포넌트가 되어서 각자에서 적용 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import type { Metadata } from "next";

/** 2023/04/30 - 메타 데이터 기본 형태 - by 1-blue */
export const defaultMetadata: Metadata = {
  title: "blegram",
  description: "인스타그램 클론 사이트입니다. ( by 1-blue )",
  generator: "Next.js",
  applicationName: "blegram",
  referrer: "origin-when-cross-origin",
  colorScheme: "dark",
  authors: [{ name: "1-blue", url: "https://github.com/1-blue" }],
  creator: "1-blue",
  keywords: [
    "1-blue",
    "TypeScript",
    "Next.js",
    "react-query",
    "prisma",
    "aws-s3",
    "aws-rds, instagram",
    "인스타그램 클론",
    "front-end, 프론트엔드",
    "신입 개발자",
    "개발자",
  ],

  openGraph: {
    title: "blegram",
    description: "인스타그램 클론 사이트입니다. ( by 1-blue )",
    siteName: "blegram",
    images: ["/logo.png"],
    locale: "ko-KR",
    type: "website",
  },
  twitter: {
    card: "summary_large_image",
    title: "blegram",
    description: "인스타그램 클론 사이트입니다. ( by 1-blue )",
    creator: "1-blue",
    images: ["/logo.png"],
  },
};

// type
interface GetMetadataProps {
  title: string;
  description: string;
  images?: string[];
  url?: string;
}

/** 2023/05/07 - 메타데이터 제조기 - by 1-blue */
export const getMetadata = ({
  title,
  description,
  images,
  url,
}: GetMetadataProps): Metadata => ({
  ...defaultMetadata,
  title: "blegram | " + title,
  description: description + "\n" + "인스타그램 클론 사이트 ( by 1-blue )",

  // og
  openGraph: {
    ...defaultMetadata.openGraph,
    title: "blegram | " + title,
    description: description + "\n" + "인스타그램 클론 사이트 ( by 1-blue )",
    images: images ? images : ["/logo.png"],
    url: process.env.BASE_URL + url,
  },

  // twitter
  twitter: {
    ...defaultMetadata.twitter,
    title: "blegram | " + title,
    description: description + "\n" + "인스타그램 클론 사이트 ( by 1-blue )",
    images: images ? images : ["/logo.png"],
  },
});

💀 문제

현재 로그인 방식이 cookieJWT를 동봉해서 처리를 하고 있습니다.
근데 SSR을 적용하면 서버측에서 실행되다보니 브라우저에 존재하는 cookie를 자동으로 넣어서 보내주지 않아서 유저 정보가 필요한 요청에 대해서 잘못된 정보를 내려받게 됩니다.
Fetch API에 쿠키를 강제적으로 넣어보려고 했는데 안들어가서 어떻게 해야할지 모르겠어서 일단은 클라이언트에서 다시 같은 요청을 보내도록 처리했습니다.

headerauthorization으로 토큰을 직접 넣어서 주는 방식으로는 토큰이 전달되는 것을 확인했는데 이미 cookie를 이용해서 처리하는 로직을 모두 구성해놔서 다른 로직을 추가하기 보다는 쿠키를 강제로 넣는 방법으로 처리하고 싶어서 기록만 해두고 넘어가려고 합니다.
마지막 배포할 때까지 방법을 못 찾으면 header를 통해서 전달하도록 처리하려고 합니다 🥲

1️⃣ og:url

원래는 metadata.openGraph.url을 사이트의 기본 URL로 지정했었습니다.
이렇게 적용하고 kakao developer 공유 디버거에서 확인해보니 https://blegram.vercel.app이던 https://blegram.vercel.app/post?postIdx=5이던 즉 루트 경로 + /뒤에 어떤 경로가 와도 자꾸 루트 경로의 metadata를 가져와서 사용하게 되는 문제가 발생했습니다.

어디서 메인으로 리다이렉트가 되는지 아니면 다른 문제인지 하나씩 바꿔가면서 배포하고 테스트하고를 반복하다보니 og:url에서 루트 경로로 설정해서 발생된 문제인 것을 확인했습니다.
og:url에는 페이지를 대표하는 URL을 적으면 되는 것으로 알고 있었는데 그게 아니라 해당 페이지의 URL을 적어야 하네요… 🥲

📮 레퍼런스

  1. Next.js 공식문서
  2. Next.js 공식문서 - 서버/클라이언트 컴포넌트
  3. Next.js 공식문서 - Fetching
  4. Next.js 공식문서 - metadata
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.