Next.js 13과 리액트 18 (2)

keemsebeen·2024년 12월 31일

모던 리액트 Deep Dive

목록 보기
18/18

11.3 Next.js에서의 리액트 서버 컴포넌트

기본적인 서버 컴포넌트의 제약은 동일하다. 서버 컴포넌트는 클라이언트 컴포넌트를 불러올 수 없으며, 클라이언트 컴포넌트는 서버 컴포넌트를 children props로 받는 것만 가능하다.

루트 컴포넌트는 무조건 서버 컴포넌트가 된다고 언급했는데, Next.js의 루트 컴포넌트는 각 페이지에 존재하는 page.js다. layout.js도 마찬가지로 서버 컴포넌트로 작동한다.

새로운 fetch 도입과 getSercerSideProps, getStaticProps, getInitialProps의 삭제

모든 데이터 요청을 웹에서 제공하는 표준 API인 fetch 를 기반으로 이뤄진다.
fetch API를 확장해 같은 서버 컴포넌트 트리 내에서 동일한 요청이 있다면 재요청이 발생하지 않도록 요청 중복을 방지했다.

SWR과 React Query와 비슷하게, fetch 요청에 대한 내용을 서버에서는 렌더링이 한번 끝날때까지 캐싱하게 된다.

정적 렌더링과 동적 렌더링

getStaticProps을 활용해 서버에서 불러오는 데이터가 변경되지 않는 경우에 정적으로 페이지를 만들어 제공할 수 있는 기능이 있었다. 이 경우 CDN에서 캐싱해 기존 서버 사이드 렌더링보다 더 빠르게 데이터를 제공할 수 있다는 장점이 있었다.

이제는, 기본적으로 빌드 타임에 렌더링을 미리 해두고 캐싱해 재사용할 수 있게끔 해뒀고, 동적인 라우팅에 대해서는 서버에 매번 요청이 올 때마다 컴포넌트를 렌더링하도록 변경했다.

캐싱을 원하지 않는다면, fetch 요청에 옵션을 설정할 수 있다.

// 기본적으로 getStaticProps와 유사하게 불러온 데이터를 캐싱해 해당 데이터로 관리
const res = await fetch( URL, { cache : 'force-cache'})

// 캐싱하지 않고 매번 새로운 데이터를 불러온다.
const res = await fetch( URL, { cache : 'no-cache'})

동적인 주소이지만 특정 주소에 대해서 캐싱하고 싶은 경우, 과거의 getStaticPaths를 흉내내고 싶다면 어떻게 해야 할까? generateStaticParams를 사용하면 된다.

export async function generateStaticParams() {
	return [{ id : '1'}, { id : '2'},{ id : '3'},{ id : '4'}]
}

캐시와 mutating, 그리고 revalidating

Next.js는 fetch의 기본 작동을 재정의해 {next : { revalidate?: number | false }} 를 제공하는데, 이를 바탕으로 데이터의 유효시간을 정해두고 시간이 지나면 데이터 패칭이 가능하다.

해당 기능은 페이지에 revalidate 라는 변수를 선언해 페이지 레벨로 정의하는 것도 가능하다.

// app/page.tsx
export const revalidate = 60

60초 간격으로 갱신해 새로 페이지를 렌더링하게 된다. 갱신이 이뤄지는 과정은 다음과 같다.

캐시를 무효화하고 싶다면 router에 추가된 refresh 메소드로 router.refresh();를 사용하면 된다. 이는 브라우저를 새로 고침하는 등 브라우저의 히스토리에 영향을 미치지 않고, 오로지 서버에서 루트부터 데이터를 전체적으로 가져와서 갱신하게 된다.

스트리밍을 활용한 점진적인 페이지 불러오기

과거 서버 사이드 렌더링 과정과 하이드레이션 과정의 순차적 과정의 지연시간을 해결하기 위해 HTML을 작은 단위로 쪼개서 완성되는 대로 클라이언트로 점진적으로 보내는 스트리밍이 도입됐다.

스트리밍을 활용하면 모든 데이터가 로드될 때까지 기다리지 않더라도 먼저 데이터가 로드되는 컴포넌트를 빠르게 보여주는 방법이 가능하다. TTFB와 FCP을 개선하는데 큰 도움을 준다.


스트리밍을 활용하면, 컴포넌트가 완성되는 대로 사용자는 페이지가 완성될 때까지 기다리는 지루함을 덜 수 있고, 페이지가 로딩 중이라는 인식을 더 명확히 심어줄 수 있다.

Loading이 Suspense를 기반으로 만들어진 Next.js의 규칙이기 때문에 Suspense를 사용하는 것도 동일한 혜택을 누릴 수 있다.

11.4 웹팩의 대항마, 터보팩의 등장(beta)

터보팩은 웹팩 대비 최대 700배, Vite 대비 최대 10배 빠르다고 하며, 앞서 소개한 라이브러리와 마찬가지로 러스트 기반으로 작성됐기 때문에 가능하다고 한다.

아직 개발 모드에서만 제한적으로 사용이 가능하다.

11.5 서버 액션(alpha)

이 기능은 API를 굳이 생성하지 않더라도 함수 수준에서 서버에 직접 접근해 데이터 요청 등을 수행할 수 있는 기능이다. 서버 컴포넌트와 다르게 특정 함수 실행 그 자체만을 서버에서 수행할 수 있다는 장점이 있다.

사용 방법

  1. next.config.js에서 기능을 활성화 한다.

    const nextConfig = {
    	experimental : {
    		sercerActions: true,
    	},
    }
  2. use server 지시자를 선언하다. 함수는 반드시 asycn여야 한다.

    asycn function serverAction() {
    	"use server";
    }

form의 action

예제에서는 handleSubmit 이라는 서버 액션을 만들어 props로 넘겨줬다. 이벤트를 발생시키는 것은 클라이언트지만 실제로 함수 자체가 수행되는 것은 서버가 된다.

export default function Page() {
	asycn function handleSubmit() {
		"use server"
		const res = await fetch(URL, { ... })
	}
	return (
		<form action={handleSubmit}>
			<button type="submit">요청 보내기</button>
		</form>
	)
}

위 코드 실행 결과 클라이언트에서는 현재 라우트 주소와 ACTION_ID만 보내고 그외에는 아무것도 실행하지 않는다. 서버에서는 요청받은 라우트 주소와 ACTION_ID를 바탕으로, 실행해야 할 내용을 찾고 이를 서버에서 직접 실행한다.

use server 로 선언돼 있는 내용을 빌드 시점에서 미리 클라이언트에서 분리시키고 서버로 옮김으로써 클라이언트 번들링 결과물에는 포함되지 않고 서버에서만 실행되는 서버 액션을 만드는 것이다.

중요한 것은 모든 과정이 페이지 새로고침이 없이 수행된다는 것이다.
server mutation으로 실행할 수 있는 함수는 다음과 같다.

// redirect, 특정 주소로 리다리엑트 할 수 있다.
import { redirect } from 'next/navigation'

// revalidatePath, 해당 주소의 캐시를 즉시 업데이트 한다.
import { revalidatePath } from 'next/cache'

// revalidateTag, fetch요청을 특정 태그 값으로 구분할 수 있다.
import { revalidateTag } from 'next/cache'

input의 submit과 image의 formAction

input type=”submit” 또는 type=”image”에 prop으로도 서버 액션을 추가할 수 있다.

startTransition과의 연동

useTransition을 사용하면 얻을 수 있는 장점 중 하나는 이전과 동일한 로직을 구현하면서도 page 단위의 loading.jsx를 사용하지 않아도 된다는 것이다. isPending을 활용해 startTransition으로 서버 액션이 실행됐을 때 해당 버튼을 숨기고 로딩 버튼을 노출함으로써 페이지 단위의 로딩이 아닌 좀 더 컴포넌트 단위의 로딩 처리도 가능해진다.

server mutation이 없는 작업

server mutation이 필요하다면 반드시 서버 액션을 useTransition과 함께 사용해야 하지만 별도의 server mutation을 실행하지 않는다면 바로 이벤트 핸들러에 넣어도 된다.

export default function Page() {
	asycn function handleSubmit() {
		"use server"
		// server mutation이 필요 없는 작업
	}
	return <button onClick={handleClick}>요청</button>
}

서버 액션 사용 시 주의할 점

  • 클라이언트 컴포넌트에서 서버 액션을 쓰고 싶을 때는 앞의 startTransition 예제처럼 use server 로 서버 액션만 모여 있는 파일을 별도로 import 해야 한다.
  • 서버에서만 실행될 수 있는 자원은 반드시 파일 단위로 분리해야 한다.

11.6 그 밖의 변화

프로젝트 전체 라우트에서 사용가능한 미들웨어가 강화됐고, SEO를 쉽게 작성할 수 있는 기능이 추가되었다.

11.7 Next.js 13 코드 맛보기

getServerSideProps 와 비슷한 서버 사이드 렌더링 구현해보기

서버 컴포넌트에서 fetch를 수행하고, 이 fetch에 별다른 cache 옵션을 제공하지 않는다면 기존 getServerSideProps 와 유사하게 작동한다.

import { fetchPostById } from '#services/sercer'

export default async function Page({ 
  parmas, 
}: {
  params : { id: string }
  children?: ReactNode 
}) {

  const data = await fetchPostById(params.id, { cache : 'no-cache'})
  
  return (
		<div>
		<h1>{data.title}</h1>
		</div>  
  )
}

미리 렌더링되어 완성된 HTML이 내려오는 것을 확인할 수 있다.

리액트 18에서는 서버 컴포넌트에서 렌더링한 결과를 직렬화 가능한 데이터로 클라이언트에 제공하고, 클라이언트는 이를 바탕으로 하이드레이션을 진행하게 된다.

getStaticProps 와 비슷한 정적인 페이지 렌더링 구현해보기

Next.js 13에서 app 디렉터리가 생겨나면서 getStaticPropsgetStaticPaths 는 사라졌지만 이와 유사한 방식을 fetch의 cache를 이용해 구현할 수 있다.

import { fetchPostById } from '#services/sercer'

export async function generateStaticParams() {
	return [{ id : '1' }, { id : '2' }, { id : '3' }]
}

export default async function Page({ parmas }: { params : { id: string }}) {

  const data = await fetchPostById(params.id)
  
  return (
		<div>
		<h1>{data.title}</h1>
		</div>  
  )
}
  1. generateStaticParams 를 사용해 /app/ssg/[id]에서 [id]로 사용 가능한 값을 객체 배열로 모아뒀다. 그리고 이 각각의 id를 props로 받을 때 어떻게 작동할 지 미리 정해뒀다.
  2. fetchPostById 에 아무런 옵션을 주지 않았다.
    1. Next.js에서 사용하는 fetch에서 줄 수 있는 cache 옵션은 다음과 같다.
      1. force-cache : 캐시가 존재한다면 해당 캐시 값을 반환하고, 캐시가 존재하지 않으면 서버에서 데이터를 불러와 가져온다.
      2. no-store : 캐시를 절대 사용하지 않고, 매 요청마다 새롭게 값을 불러온다.

이처럼 정적으로 미리 빌드해 두는 것뿐만 아니라 캐시를 활용하는 것도 가능하다.

정적으로 생성된 페이지를 점진적으로 갱신하는 것을 의미한다.(ISR)

import { fetchPostById } from '#services/sercer'
export const revalidate = 60
export async function generateStaticParams() {
	return [{ id : '1' }, { id : '2' }, { id : '3' }]
}

export default async function Page({ parmas }: { params : { id: string }}) {

  const res = await fetch(`https://json.com/post/{id}`)
  const data: Post = await res.json()
  
  return (
		<div>
		<h1>{data.title}</h1>
		</div>  
  )
}

로딩,스트리밍,서브펜스

직접 Suspense로 감싸 부분적으로 로딩을 보여주는 것 외에도 기본적으로 loading이라고 하는 파일 예약어를 지원하면서 손쉽게 로딩 라우팅별로 로딩 상태를 나타낼 수 있도록 제공한다. 둘다 동일한 방식으로 작동하며, Suspense 가 조금 더 개발자가 원하는 형태로 쪼개서 보여줄 수 있다는 차이만 있다.

profile
프론트엔드 공부 중인 김세빈입니다. 👩🏻‍💻

0개의 댓글