blan19.com

나는 왜 Next.js@14를 사용했을까?

December 20, 2023 (1y ago)

......

배경

주니어 개발자로서 겪은 시행착오와 생각들을 정리하기 위해 만들어 두었던 블로그를 1년 동안 관리하지 않았습니다. 이제는 더 이상 블로그 관리를 미룰 수 없다고 느끼게 되었고, 평소에 정리해둔 자료들과 학습한 내용이 휘발되는 것을 막고자 결심했습니다.

기존의 레거시 블로그 프로젝트는 next.js@12를 기반으로 제작되었습니다. 그러나 관리를 안한지 1년이라는 사이에 next.js는 12에서 14로 메이저 버전이 업데이트되며 상당한 변화가 있었습니다.

기존의 레거시 코드를 관리하기 위해 두 가지 선택지를 고려했습니다.

  • next.js@12의 코드 베이스를 유지하면서 부분적으로 next.js@14를 도입하는 방법
  • next.js@14로 전체적으로 마이그레이션하는 방법

결국엔 next.js@14로 모두 마이그레이션하는 방법을 선택했습니다.

next.js@12를 그대로 유지하면서 page router와 app router를 모두 사용하면 마이그레이션 작업이 비교적 간단할 것으로 예상했습니다. 또한, next.js를 사용하는 개발자 커뮤니티에서는 13버전부터 큰 변화로 인해 학습 곡선이 크다는 의견이 있었지만, 이러한 의견에도 불구하고 next.js@14로의 전체 마이그레이션을 선택했습니다.

이러한 선택에 대한 이유를 순차적으로 정리해보겠습니다.

Vercel 팀에서 제공하는 Next.js 학습 코스

Next.js Learn은 Vercel의 Next.js 팀에서 제공하는 최신 Next.js에 대한 학습 프리코스입니다.

최근 커뮤니티에서 학습곡선이 있다는 의견들을 의식해서 그런지 쉽게 배울 수 있도록 도움이 되는 프리코스를 오픈했습니다.

현재 회사에서 진행 중인 프로젝트는 next.js를 사용하지 않기 때문에 next.js@12까지의 기억만 가지고 있는 저로서는 처음부터 다시 배워야 하는 입장이었습니다.

13버전에 큰 변화가 있어 자칫 잘못하면 배우는 시간에만 큰 비용이 소모될 수도 있었습니다. 그러던 차에 next.js 팀에서 학습을 위한 프리코스를 제공하는 것을 확인했고, 프리코스를 차례차례 학습하며 부족한 부분은 공식문서로 채우면 큰 시간 소모 없이 금방 학습할 수 있다고 판단하였습니다.

Learn
Learn Quiz

자세한 예시코드와 중간마다 퀴즈도 포함되어있어 대략 2 ~ 3시간 만에 새롭게 바뀐 next.js에 적응할 수 있었습니다.

프리코스 자체 퀄리티가 높아 처음 next.js를 배우는 사람들도 따로 강의를 구매할 필요 없이 공식문서 + 프리코스면 충분해 보였습니다.

Server actions

블로그를 새롭게 단장하면서 추가될 기능 중 하나가 게시글의 조회수를 볼 수 있는 기능이었습니다. 이를 위해선 데이터베이스를 사용을 필수였습니다.

현재 블로그는 vercel에서 호스팅 되고 있는데, vercel의 기능 중 하나인 storage를 사용하여 쉽게 데이터베이스를 사용할 수 있었습니다. 또한 vercel에서 SQL injections에 대해서 방지도 해주고 있습니다.

next.js@14에 추가된 server actions를 사용한다면 db 처리를 안정적으로 서버 사이드에서 처리를 할 수가 있게 되었습니다.

Server actions은 React Canary에 포함된 실험적인 기능들을 Next.js 팀에서 안정화해 14버전에 도입시켰습니다.

Server actions를 사용하는 방법은 간단합니다. `"use server"` 키워드를 상단에 인라인으로 적으면 끝입니다.

// 서버 컴포넌트
const UserFormPage = () => {
  const signUp = async (form: FormData) => {
    // 이 작업은 서버 사이드에서 이루어집니다.
    "use server";
    await auth.signUp(form);
  };

  return (
    <form action={signUp}>
      <input type="email" name="email">
      <input type="password" name="password">
    </form>
  );
};

export default UserForm;

next.js에서는 기본적으로 페이지를 서버 컴포넌트로 렌더링합니다. 클라이언트 컴포넌트에서 Server actions를 사용하려면 Server actions를 따로 모듈로 분리하여 import 하여 사용해야 합니다.

// /src/apps/db/action
"use server"

const signUp = (form: FormData) => {
  ...
}

export { signUp }

// /src/components/ui/button
"use client"

import { signUp } from "@/apps/db/action";

const Button = () => {
  return (
    <button type="submit" onClick={signUp} />
  );
};

export default Button;

Server actions 덕분에 서버사이드의 작업을 별도의 API route 작성 없이 가능해졌습니다. 또한 개인적으로 Server actions를 사용하면서 모듈화를 함으로서 관심사 또한 분리하기 쉬워졌습니다.

├── apps
│   ├── blog
│   ├── db
│   │   ├── actions // action 작업들은 여기로
│   │   └── queries // query 작업들은 여기로
.
.
.
// /src/apps/db/quries
"use server";

const getViewCount = async (slug: string) => {
  if (!process.env.POSTGRES_URL) {
    return null;
  }

  noStore();

  const view = await prisma.view.findUnique({
    where: {
      slug,
    },
  });

  return view;
};

export { getViewCount };

// /src/apps/db/actions
("use server");

const increment = async (slug: string) => {
  noStore();

  await prisma.view.upsert({
    where: {
      slug,
    },
    update: {
      count: {
        increment: 1,
      },
    },
    create: {
      slug,
    },
  });
};

export { increment };

이 밖에도 제가 체감하지 못한 Server actions의 강력한 효과는 더 많다고 생각합니다.

Partial Static Prerendering

next.js@14을 학습하면서 가장 흥미롭게 보았던 부분입니다. 실제로 제 블로그에 적용까지 해보았는데, next.js 팀에서 웹 어플리케이션의 기본 렌더링이 될 잠재력이 있다고 기대하는지 알 수 있었습니다.

`Partial Static Prerendering`은 말 그대로 정적 렌더링과 동적 렌더링의 결합입니다. 가끔 next.js를 사용하면서 이런 생각이 들 때가 있었을 수도 있습니다.

"SSG 와 SSR 둘 다 사용할 수는 없나?"

이에 대한 갈증을 부분적으로 해결해 주었던 것은 기존의 ISR이였지만, ISR의 부족한 부분을 채워 등장한 것이 Partial Static Prerendering 입니다.

  • Build 타임에 페이지의 정적인 부분들은 Prerendering 됩니다. (기존 SSG 처럼 정적인 부분은 캐시 되어 다음 요청 시에 재사용됩니다.)
  • 초기 페이지 로드 후 동적인 부분은 Streaming을 사용하여 hydration이 됩니다.

Partial Static Prerendering을 통해 페이지에서 정적인 부분을 빠르게 렌더링을 하고 동적인 부분을 Streaming을 통해 병렬적으로 가져와 사용자 경험을 높일 수 있습니다.

Partial Static Prerendering은 아직 next.js에서 실험적인 기능입니다.
앞으로 next.js의 마이너 릴리즈 버전에서 더 많은 업데이트가 이루어질 수 있습니다.

Partial Static Prerendering을 사용하는 법을 알아보겠습니다. 아직은 실험적인 기능이기 때문에 먼저 next.js의 canary 버전을 설치해야 합니다.

npm install next@canary

설치가 완료되었다면 next.config.js에서 옵션을 활성화해야 합니다.

// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    ppr: true,
  },
};

module.exports = nextConfig;

이제 Partial Static Prerendering이 사용 가능해졌습니다!

아직 cliend-side navigation에는 아직 적용이 안됩니다. (next.js 팀에서 노력하고 있다고하니 조금만 기다려주세요!)
또한 node.js node.js 런타임으로 설계되어 있습니다

제 블로그의 경우에는 메인 홈에서 블로그 게시글에 대한 조회수를 보여주면서 이 부분은 동적 렌더링이 필요했습니다.

View
블로그 조회수 뷰

Partial Static Prerendering을 사용해서 조회수를 보여주는 블로그 게시글은 Streaming을 사용하여 병렬적으로 동적 렌더링을 해주었습니다

const View = async ({ slug }: { slug: string }) => {
  const view = await getViewCount(slug);

  return (
    <ViewCounter
      className="text-xs md:text-sm text-greyscale-7 dark:text-greyscale-2"
      view={view?.count ?? 0}
    />
  );
};

const Blogs = async () => {
  // ...
  return (
    <ul>
      {posts
        .filter((post) => blogs.find((blog) => blog.slug == post.slug))
        .map((post) => (
          <li key={post.slug}>
            <Link href={`/blog/${post.slug}`}>
              <Card
                title={post.metadata.title}
                tags={post.metadata.tags}
                date={post.metadata.publishedAt}
              >
                <View slug={post.slug} />
              </Card>
            </Link>
          </li>
        ))}
    </ul>
  );
};

export default function Home() {
  const posts = getBlogPosts("tech");

  return (
    <section className="grid gap-12">
      // ...
      <div>
        <h1 className="font-medium text-2xl mb-4 tracking-tighter">
          최근 블로그 포스팅
        </h1>
        <Suspense fallback={<Skeleton />}>
          <Blogs />
        </Suspense>
      </div>
      <div>
        <h1 className="font-medium text-2xl mb-4 tracking-tighter">
          조회수가 가장 높은 블로그 포스팅
        </h1>
        <Suspense fallback={<Skeleton />}>
          <Blogs />
        </Suspense>
      </div>
      // ...
    </section>
  );
}

블로그 게시글 부분을 제외한 나머지는 정적으로 렌더링 되어 빠른 페이지 렌더링 경험은 그대로 유지하고 동적으로 데이터도 보여줄 수 있어서 매우 만족스러웠습니다.

Partial Static Prerendering을 사용하는데 새로운 API의 사용이 필요가 없어 학습하고 사용하는데 큰 어려움도 없습니다.

마치면서

1년도 안 돼서 next.js가 정말 많이 변했습니다.
이렇게 매년 빠르게 변화하는 것은 두려워질 수 있지만, 한편으로는 한 분야에 고정돼 있지 않고 유연한 사고를 유지하는 원동력이 될 수 있다고 생각합니다.
그래서 오히려 흥미진진하게 변화를 지켜보고 있습니다. 또한 내년에는 어떤 발전과 새로운 개념이 등장할지를 기대되고 두근거리네요.

글 작성에 도움을 준 레퍼런스들