Next.js

Vercel 이 지들 컨퍼런스 열리는 24일이 채 되기도 전에 부랴부랴인지 뭔지 모르겠지만 Next.js 15 버전을 대뜸 출시했다.
현지하지만, React 19 버전은 RC이다.

참고로 불행 중 다행인 것은, 페이지 라우터 쓰는 개발자들은 React 18 버전 유지하면서 15 버전 업그레이드가 가능하다는 것.

그럼, 좋아. 지금 어떤 꼬라지인지 함께 보도록 하자.

Next.js 15 변경점

공식 블로그에서 소개한 주요 변경점을 번역하겠다.

  • @next/codemod CLI: Next.js 및 React 최신 버전에 맞게 손쉽게 코드 업그레이드 지원
  • 비동기 요청 API (Breaking): 렌더링 및 캐시 모델의 단순화로 인한 결정
  • 캐시 방식 변경 (Breaking): API 요청 시(fetch), GET 라우팅 핸들러, 클라이언트 탐색에서 기본값으로 캐시를 사용하지 않음.
  • React 19 지원: React 19 기능 및, React 컴파일러 (Experimental), 하이드레이션 오류 추적 향상.
  • Turbopack 개발 (안정화): 성능과 안정성 향상된 개발 환경.
  • 정적 표시기: 개발하는 동안 정적 라우팅의 시각적 표시 기능 추가.
  • unstable_after API (실험적): 응답이 끝난 후 후처리 기능 추가.
  • instrumentation.js API (Stable): 서버 생명 주기를 감시하는 안정적인 API.
  • 향상된 양식 컴포넌트 (next/form): HTML form 태그에 클라이언트 탐색 기능 추가.
  • next.config: next.config.ts 파일도 이제 TypeScript 지원.
  • 자체 호스팅 환경 향상: Cache-Control 헤더 조정 등 향상.
  • 서버 액션 보안: 끝점을 통한 서버 액션 유추를 방지하고 사용하지 않는 액션은 빌드 시 제거.
  • 외부 패키지 번들링 (Stable): 앱 및 페이지 라우터의 패키지 번들 옵션 추가.
  • ESLint 9 지원.
  • 개발 및 빌드 성능 향상: 새로고침 속도 향상 및 빌드 속도 증대.

여기서 눈여겨볼 만한(이라 읽고 내가 쓴다) 변경 사항만 자세히 설명해보겠다.
사실 대부분의 기능들은 앱 라우터에서 변경할 사항이 많다.

비동기 요청 API

서버 컴포넌트나 라우팅 핸들러, 서버 액션에서 헤더 및 쿠키를 가져올 때 사용하는 API가 있다.

import { headers, cookies } from 'next/headers';

여태까지 그냥 함수 호출했는데, 이제는 강제 비동기 행이다. 즉, await 붙이든가 then 콜백 쓰든가 해서 비동기로 호출해야 한다.

// Next.js 14 이전
const header = headers();
// Next.js 15 이후
const header = await headers();

만약 비동기로 호출하지 않을 경우 개발 시 콘솔에서 warning 이 뜬다고 한다. 작동은 된다고 한다. 완전 막은 것은 아니다.

이와 함께 앱 라우터에게 절망을 하나 선사했는데, 페이지 컴포넌트(page.js)에서 받을 수 있는 라우팅 인자(params)와 검색 인자(searchParams) 또한 비동기가 강제된다. 따라서 아래 둘 중 하나의 함수 선언을 해야 한다.

  1. 비동기 함수 컴포넌트 (서버 컴포넌트에 추천)
// 예시: /my/page/[name]
// 접속: /my/page/foo?name=bar
export default async function MyPageComponent({ params, searchParams }) {
  return <ul>
    <li>네가 조회한 라우팅 명칭: { params.name }</li>
    <li>네가 조회한 검색 조건: { searchParams.name }</li>
  </ul>;
}
  1. use 훅을 사용한 동기 함수 컴포넌트 (클라이언트 컴포넌트에 추천)
'use client';
import { use } from 'react';
// 예시: /my/page/[name]
// 접속: /my/page/foo?name=bar
export default function MyPageComponent(props) {
  const params = use(props.params);
  const searchParams = use(props.searchParams);
  return <ul>
    <li>네가 조회한 라우팅 명칭: { params.name }</li>
    <li>네가 조회한 검색 조건: { searchParams.name }</li>
  </ul>;
}

서버 액션과 라우팅 핸들러에 설마 동기 함수 고집하는 일은 없겠지? 믿고 넘어가도록 하겠다.

캐시 방식 변경

귀찮아서 짧게 설명하겠다. 이제 서버 컴포넌트 및 GET 라우터에 본문 시작 전 export const dynamic = 'force-dynamic 이지랄 할 필요 없다. 기본값이다.
하지만 그래도 난 캐시해야 한다면, export const dynamic = 'force-cache 쓰면 된다. 그럼 이전 버전 기본값처럼 동작한다.
참고로 POST 등의 변경요청 가능한 핸들러와 서버 액션은 처음부터 캐시 안하고 특성 상 하지도 못하니 그리 알도록 한다.

React 19 지원

당장 벨로그 돌아다녀도 React 19 관련 글 많으니까 그걸로 대신하기 바란다. 설명 귀찮네 증말.

React 컴파일러 (실험적)

참고로 이거 쓰고 싶을 경우 SWC 버리고 Babel 써야 한다. 왜냐면 아직 Babel 변환기만 지원하기 때문이다.
물론 작업 하고 있다고 하니 다음 버전을 기다려 보도록 하자...?!
그래도 난 존나 힙하거나 모험, 또는 좋은 행동인 기여를 하고 싶다면 공식 메뉴얼을 참고하라.

after (실험적)

비즈니스에서 많이 요구하는 기능이 드디어 실험적이라도 나왔다. 나도 당장 쓰고싶은 생각에 목마를 지경이다.
어쨌든, 그냥은 못 쓰고, 세팅해야 하기 때문에 사용 방법은 공식 메뉴얼을 참고하기 바란다. 사용 예시는 아래와 같다.

import { unstable_after as after } from 'next/server';
import { getBoardDetail, increaseBoardCount } from '@/server/db/board';

// 게시판 상세 페이지에 대한 서버 컴포넌트
export default async function BoardDetailPage({ params: { boardSeq } }) {
  // 부 업무
  after(async () => {
    // 게시물을 읽었으면 조회수를 늘려야지.
    await increaseBoardCount(boardSeq);
  });
  
  // 게시물 상세 정보 취득
  const detail = await getBoardDetail(boardSeq);
 
  // 주 업무
  return <>{/*여기에 게시물 상세 구조 작성*/}</>;
}

사용 가능한 곳은 서버 컴포넌트, 메타데이터, 서버 액션, 라우팅 핸들러, 그리고 middleware 이다.
이게 생기는 이점이라면, 바로 자체 호스팅이냐 클라우드냐 따로따로 고민할 필요 없이 프레임워크 단에서 한방에 후처리를 해결할 수 있는 점이 되겠다.

instrumentation.js

사실 나도 이거 필요했다. 배포한 뒤에 서버 오류를 발견하는 건 꽤 귀찮고 힘든 일이었다.
하지만 Next.js 15 부터는 이런 귀찮은 일을 한방에 해결해 줄 기능이 드디어 안정화가 됐다.

사용 방법은 middleware.js 와 같은 폴더에 instrumentation.js (또는 ts 파일) 만들고 아래 2개 함수를 작성하면 된다.
자세한 사용 방법은 공식 메뉴얼 참고.

export async function onRequestError(err, request, context) {
  // 여기에 매 요청 시 오류났을 때 업무를 구현하도록 한다.
  // err: 오류 객체(err.message 로 오류 내용 출력)
  // request: 일반 Request 객체보다 너무 축약된 { path, method, header } 만 제공한다.
  // context: 호출 주체인데, 이건 좀 설명하기 복잡하므로 공식 메뉴얼 참고하라.
}
 
export async function register() {
  // 여기에 위 onRequestError 호출 전에 '초기화'해야 하는 업무를 구현하면 된다.
}

이 기능을 구현하기 위해 Sentry 라고 앱 모니터링 서비스 업체와 협업했다고 한다.
한국으로 치면 대충 제니퍼 비슷한 곳.

<Form /> 컴포넌트

Next.js 차원에서 양식 이동 시에도 클라이언트 탐색을 지원하는 <Form> 컴포넌트를 제공하기 시작했다.
이녀석이 왜 있냐고 물어본다면, 주로 조회 페이지에서 검색 양식을 만들 때에 유용하다고 생각하면 쉽다.

사용하고 싶다면, 공식 메뉴얼에 사용 방법을 숙지하도록 한다.

이거 없었을 때 검색 양식 업무를 구현하려면 기존 <form> 태그에 onSubmit 에다가 e.preventDefault() 먼저 걸고 이것저것 로직 처리한 다음 useRouter 훅에서 초기화한 라우팅 이동 함수에다가 라우팅 경로와 인자를 전달하여 처리하는 난리 부르스를 쳤을 것이다.
그리고, 이 컴포넌트로 인해 서버 컴포넌트에서도 손쉽게 검색 양식 같은 업무를 작성할 수 있게 됐다.
특히 이걸 좀 더 발전시켜서 zod 같은 유효성 검사 라이브러리와 시너지 작용도 기대할 수 있다.

자체 호스팅 기능 향상

Next.js 에서 자체 호스팅 기능을 향상했다고는 하는데, 대충 내용은 이렇다.

  • expireTime config 속성을 추가하여 별도 전역 헤더 설정 없이 캐시 만료 시각을 손쉽게 설정 가능. 기본값은 1년.
  • Next.js 자체 호스팅 운영 시 필요했던 sharp를 별도로 설치할 필요가 없어졌고 Next.js 가 알아서 감지해서 필요 시 작동하도록 바뀌었다.

대충 CDN 관련 설정에 대한 불만 및 이슈를 제기해서 나온 결과라 보면 되겠다.

서버 액션 보안성 향상

주로 보안 쪽에서 많이 제기한 서버 액션에 대한 악용에 대해 많은 피드백 덕분인지 서버 액션이 이제 아래와 같이 동작하게 된다.

  • 서버 액션을 호출할 때, 웹 상에서 경로 호출 시 무작위 ID를 부여받아 이를 호출하도록 향상하여 기본적인 CSRF 대응책을 제공한다.
  • 빌드 시 사용하지 않는 서버 액션을 삭제하여, 운영 시 존재하지 않는 서버 액션을 호출하여 정보를 빼가는 일이 없어졌다.

첫번째가 뭔 소린지 모르겠다면, 공식 메뉴얼을 보도록 한다. 네가 평소 CSRF 방어를 위해 조치했던 거와 비슷하다고 보면 된다. 물론 이것도 걱정되면 더해도 된다.

외부 패키지 번들링 옵션

next.config.js 에서,

  • 페이지 라우터의 경우, bundlePagesRouterDependencies 옵션을 설정하면 앱 라우터처럼 외부 패키지 번들링이 이루어진다.
  • 페이지 라우터(위 설정 시) 및 앱 라우터에서 별도로 번들링해야 할 패키지를 serverExternalPackages 배열 속성으로 설정 가능하다

이게 쓸모가 없는 건 아닌게, 주로 ssh2 같이 외부 리소스 연계와 관련된 라이브러리 및 무거운 라이브러리가 하나로 합쳐지거나, 하나로 합쳐질 때 문제가 생기는 걸 방지하기 위해 존재하는 옵션이니, 패키지 떡칠한 개발자들 특히 한번씩은 체크하기 바란다.

ESLint 9 지원

다들 알다시피 ESLint 설정 형식이 배열 방식으로 바뀌는 등 여러 변경점이 발생했는데, Next.js 14까지는 ESLint 8 당시 형식 말고는 돌아가지 않아 많은 불만이 있었는데, 드디어 이를 Next.js 15에서 해결했다.
참고로 ESLint 기존 형식에서 신규 형식으로 변환하도록 ESLint 에서 제공하고 있기는 하다.

근데 React 19는 RC

장황하게 Next.js 15 향상점에 대해 얘기를 했는데, 막상 React 19가 아직 정식 출시가 안된 상황이다.
멀리 볼 것도 없이, 당장에 큰 이유는 바로 지난번 SPA 개발자들 빡치게 한 Suspense 커밋 사건이다.

좋아. 그러면 이제 그다음 볼 곳은 바로 React 19 milestone 이다.
보니까, 10월 23일 기준 5개의 과제가 남았는데, 절반은 구형 방식에 대한 Removal 이고, 눈여겨 볼 이슈 2개가 있다면,

전자도 꽤 골치아픈 이슈인데, 제어 컴포넌트의 주요 속성인 value 속성을 동기화 할 것이냐 안할 것이냐다.
만약 한다면, form.reset() 같이 DOM 정식 API에 대한 대응이 유리해지는 등의 일반 DOM 접근성 향상이고,
만약 뺀다면, 제어 컴포넌트의 DOM 동기화로 인해 발생된 여러 react-dom 버그를 해결할 수 있다는 의견이다.

현재 상태는 전자에 해당한다. 현재 공식 팀에서 6년동안 결정한 사항이 없는 거 보니 올해 안에 출시될 지는... 장담 못할 것 같다.

두번째 이슈도 머리아프게 만드는데, 현재 JSX는 HTML 중심으로 대응되어 있으면서도 XML 구조를 강제한다. 하지만 XML 문법을 100% 활용할 수 있는 것도 아니고... 그리고 SSR 상에서는 XML 규칙을 충실히 따르고 있는 터라 아무리 느슨한 HTML이라 해도 <div/> 이지랄 했을 때 브라우저 렌더링 결과도 장담할 수 없으니...

하지만, 다행인 점은 이것들은 Semantic 이슈라 결정되지 않아도 쓰는데 큰 지장이 없는 이슈다. 이 둘의 공통점은,

기존에 이러이러 했는데, 개선할까 말까?

이 한마디로 정리하는 이슈이기 때문이다. 그리고 나머지 이슈도 레거시 제거 관련 이슈이기도 하고...

그래서 지금 React 19 RC는 사용하는데 큰 무리는 없다는 의견이 중론이다.

물론 안정화된 버전은 아니고, 게다가 최초의 메이저 버전으로 출시한다 한들 버그가 없다는 보장은 없으니.
게다가 만약 발견하면 이슈를 제기하면 되긴 하다. 근데 18에서 19로 변경하는 과정에서 생긴 이슈가 아니면 잘 안 볼 테고,
그리고 현재 이슈 개수는 685개. 물론 거르고 거르면 몇백개 되겠지만 Next.js 의 경우 이슈가 자그만치 거의 3000개 돌파.
이정도로 산업계에서 리액트와 넥스트의 인기가 실감이 된다고 볼 수 있겠다.

어쨌든, Next.js 가 이렇게 React 19 나오기도 전에 이렇게 출시한거 괜찮을까?
심지어 공식 블로그에서도 Next.js 15 is officially stable and ready for production 라는 문구까지 써가며 자신있게 내비친 의도는?

아 맞다. <Suspense> 이슈 말이지? 그거 Next.js 같은 SSR 위주로 같이 쓸때는 별 탈 없는 이슈야. 문제가 되는 부분은 SPA 기준이기 때문이지.
물론 이렇게 저지른 Vercel 이 미울 수도 있어. 근데 React 버전업에 자기들 자산까지 써가며 힘을 보태주잖아. 동병상련인가? ㅋㅋ

에라이 모르겠다

결국 용두사미 결론이 나오고 말았다. 사실 결론은 별거 없다. 쓰고싶으면 써라 다.
나도 한번 뚜껑을 열어볼 예정이고, 뭐가 어찌될 지 모르겠지만, 확실히 기능 향상 측면에서는 이전보단 더 낫거든.
이게 좋고 나쁜 선악의 관계도 아니고, 프론트엔드는 원래. 선택이란 게 가장 중요한 역할을 가진 개발자거든.
쌩 날것으로 써도 되고, React 필요하면 쓰고, Vue 필요하면 쓰고, Svelte 필요하면 쓰는 거다.
그러나 선택과 집중이 요구될 수 없는 직종이 프론트엔드 개발자긴 하지.
왜냐, 프론트엔드 기술은 너무나도 파편화가 되어 있기 때문에.

물론 리액트는 업무 중심에서의 웹 앱 구축 측면에서, 관리적 측면에서 누가 봐도 탁월한 선택이다.
하지만 프론트엔드의 특징은 결국 무거워질 경우 그 무겁다는 표현을 솔직히 드러내는 게 바로 프론트엔드다.
백엔드는 그 백엔드 앱 하나만 보면 되지만, 프론트엔드는 브라우저 눈치보고 클라이언트 눈치보고 그리고 이용하는 유저 눈치도 봐야 하지. ㅋㅋ

여기서 과연 Next.js 의 과감(?)한 결정은 옳은 선택일까?
조만간 이슈에서 보도록 하자.

끗.

profile
지옥에서 온 개발자
post-custom-banner

2개의 댓글

comment-user-thumbnail
2024년 10월 31일

안녕하세요 :)
필력이 좋으셔서 재밌게 보고 팔로우하고 갑니다!
참고해서 저도 정리해보겠습니다!
감사합니다 (_ _)

답글 달기
comment-user-thumbnail
2024년 11월 4일

qwik qwik slow

답글 달기