Front-End/Next

Next.js 리액트 서버 컴포넌트

Voyage_dev 2024. 8. 18. 16:02
✔️ 기존 SSR의 한계 및 Next.js 서버 컴포넌트의 개념에 대해서 살펴보자

 

React Server Component는 리액트 18 버전에서 도입이 되었다. 기존의 서버 사이드 렌더링과는 완전히 다른 개념이다.

 

기존 리액트 컴포넌트와 SSR의 한계

 

기존의 React 컴포넌트는 클라이언트에서 작성이 되고 브라우저에서 자바스크립트 코드에 대한 처리가 이루어지게 된다. 그래서 React로 만들어진 페이지는 유저가 웹사이트를 방문했을 때 React 실행에 필요한 코드를 먼저 다운로드 받고 React 컴포넌트 트리를 브라우저에서 만들어주게 된다. React 컴포넌트 트리를 바탕으로 DOM에서 렌더링을 진행하게 된다.

 

기존의 이런 클라이언트 사이드 렌더링 방식에서 여러 가지 한계들이나 성능상의 이슈들을 마주했기 때문에 나온 게 서버 사이드 렌더링이다. 미리 서버에서 DOM을 만들고 만든 DOM을 클라이언트에서 받아서 그 DOM에서 Hydration만 진행해 주는 식으로 DOM을 만드는 작업을 서버 쪽으로 위임을 한 방법이다.

 

기존의 React 컴포넌트와 서버 사이드 렌더링을 잘 쓰고 있었는데 어떠한 한계 때문에, 어떠한 단점 때문에 새로운 개념이 등장했는지를 알아보자

 

자바스크립트 번틀 크기가 항상 존재

import sanitizeHtml from 'sanitize-html' // 260k

function ExampleComponent({text}: {text:string}) {
	const html = useMemo(() => sanitizeHtml(text), [text])
	
	return <div dangerouslySetInnerHTML={{_html: html }} />
}
  • npm 라이브러리 import 시 번들 사이즈가 있고, 클라이언트인 브라우저에서 해당 라이브러리를 다운로드 받아야 하고 실행도 해야 한다. 이 각각의 라이브러리의 번들 사이즈만큼 부담을 안게 된다.
    • 만약 이 컴포넌트를 서버에서만 렌더링 하고 클라이언트는 결과만 받는다면?
      • 클라이언트가 이 번들 사이즈와 모든 의존성들을 받아와야 하는 부담을 서버 쪽으로 위임을 하게되면 클라이언트 입장에서는 부담이 줄어들 것이다.
    • 라이브러리는 서버에서 가지고 있고, 라이브러리를 실행한 결과와 컴포넌트 렌더링 결과물만 클라이언트에 제공

백엔드에 직접적인 접근이 불가능

import db from 'db'

async function ExampleComponent({id}: {id: string}) {
	const text = await db.board.get(id);
	
	return <>{text}</>
}
  • 기존의 코드는 클라이언트에서 직접 백엔드에 접근해서 원하는 데이터를 가져올 수가 없었다. 그러다 보니 API 로직이 필요했고 추가적인 백엔드 개발자들의 작업이 늘어났다.
  • 만약 클라이언트에서 직접 백엔드에 접근해 원하는 데이터를 가져올 수 있다면?
    • 단계가 하나 줄어들고, 백엔드도 수고로움이 줄어든다.
  • 서버 사이드 렌더링을 하면서 서버 단에서 DOM이 만들어지기 때문에 직접 컴포넌트 단에서 DB에서 데이터를 가져와도 문제가 될 것은 없다.
  • 이러한 백엔드에서 직접적인 접근을 할 수 없는 것들이 이슈가 되었고 개선하기 위해서 React 서버 컴포넌트의 개념이 나오게 되었다.

연쇄적으로 발생하는 서버와 클라이언트의 요청에 대응하기 어려움

  • 하나의 컴포넌트가 렌더링이 되고, 그 컴포넌트로 인해 또 다른 컴포넌트를 렌더링하는 시나리오를 가정해 보자
    • 최초 컴포넌트의 요청과 렌더링이 끝나기 전까지는 하위 컴포넌트의 요청과 렌더링이 끝나지 않는다는 단점
    • 서버에 요청하는 횟수도 점점 더 늘어나기 시작하면서 서버에 가해지는 부담이 굉장히 커졌다.
    • 이 작업을 모두 서버에서 작업한다면?
      • 데이터를 불러오고 컴포넌트를 렌더링 하는 것이 모두 서버에서 이루어지므로 클라이언트에서 서버 요청함으로써 발생하는 비용과 시간 지연이 줄어든다.
      • 반복되는 요청도 감소.

추상화에 드는 비용이 증가

  • 리액트는 템플릿 언어가 아니다.
    • 템플릿 언어는 HTML에서 할 수 없는 for, if 문 등을 처리 가능하나, 복잡한 추상화나 함수 사용은 어렵다.
    • 리액트는 자바스크립트 기반으로 다양한 작업이 가능하다 → 자유를 주지만, 추상화가 복잡해 질 수록 코드양이 많아지고 런타임 오버헤드도 많아진다.
    • 컴포넌트는 복잡하나, 사용자에게 전달되는 HTML은 단순할 수 있다.
  • 추상화에 따른 결과물 연산 작업을 서버에서 수행한다면?
    • 클라이언트는 복잡한 작업을 하지 않아도 되어서 속도가 빨라진다.
    • 코드 추상화 비용은 서버에서만 지불하면 된다.
    • 클라이언트, 서버 간 주고받는 결과물이 가벼워 진다.
✔️ 새로운 fetch

 

  • Next.js 12 버전까지 SSR과 SSG를 위해 사용되던 getServersideProps, getStaticProps, getInitialProps는 앱 라우터에서는 삭제되었다.

모든 데이터 요청은 웹에서 제공하는 표준 API인 fetch를 기반으로 이루어진다

 

예제 코드

import { Children } from "react";

async function getData() {
  const result = await fetch('<https://jsonplaceholder.typicode.com/posts>')

  if(!result.ok) {
    throw new Error('실패');
  }

  return result.json();
}

export default async function Page() {
  const data = await getData();

  return (
  	{data?.map((article: any) => { return (
		{article.title}
		{article.body}
    	)}
 	  )}
     )
}
  • 이런 식으로 앱 라우터 버전에서는 기존의 getServerSideProps 같은 메서드가 아니라 fetch를 가지고 굉장히 간단하게 데이터를 불러올 수 있게 되었다.
  • getServerSideProps는 SSR만을 위한 것이었으므로, 이제 서버에서 데이터를 직접 불러올 수 있게 되었다
  • 컴포넌트가 비동기적으로 작동하는 것도 가능해졌다.
    • 서버 컴포넌트는 데이터가 불러오기 전까지 기다렸다가 데이터가 불러와지면 페이지가 렌더링 되어서 클라이언트에 전달이 될 것이다.

Next.js는 fetch 요청에 대한 내용을 서버에서는 렌더링이 한 번 끝날 때 까지 캐싱. 중복된 요청을 방지한다.

  • 최초 → 1-2s
  • 그 다음 → 수십 ms
  • 매번 렌더링을 하는 것이 아니라 한 번 렌더링을 처음에 해주고 그다음에 그 값을 그대로 들고 있다가 보여줄 수가 있어서 서버 사이드 렌더링의 장점을 활용해서 보여주게 된다. 그래서 두 번째 부터는 캐싱을 적용하면 굉장히 빠르게 HTML파일을 불러올 수 있다.

정적 렌더링과 동적 렌더링

  • Next.js 12 버전
    • getStaticProps로 서버 데이터가 변경되지 않는 경우 정적 페이지를 만들어서 제공
    • 해당 주소의 결과물이 항상 동일 → CDN 캐싱으로 SSR보다 더 빠르게 제공
  • Next.js 13 버전
    • 정적 라우팅 : 빌드 타임에 미리 렌더링, 캐싱, 재사용
    • 동적 라우팅 : 서버에서 요청이 올 때마다 컴포넌트 렌더링

정적 라우팅

  • 기본은 캐싱, 하지만 캐싱하지 않는 방법도 있다.
  • 캐싱을 했을 때 단점은 데이터가 캐시 시간이 오래 지날수록 stale 하게 바뀔 가능성이 높다. 즉, 낡은 데이터를 계속 볼 수가 있다.
  • 실시간으로 가장 최신 데이터를 받아와야 되는 서비스의 경우에는 캐시 옵션을 꺼줘야 할 수도 있다.
// cache : 'no-cache' 옵션 추가
  const result = await fetch("<https://jsonplaceholder.typicode.com/posts>", {
    cache:"no-cache"
  });

// Next.js 옵션을 사용하는 것도 가능
    const result = await fetch("<https://jsonplaceholder.typicode.com/posts>", {
	    next: { revalidate: 0 }
  });
  • fetch에서 캐싱을 하지 않으면, Next.js는 해당 요청을 미리 빌드해서 대기시켜 두지 않고 요청이 올 때마다 fetch 요청 이후 렌더링을 수행하게 된다.
  • 시간은 다소 걸리지만 가장 최신의 데이터를 바로바로 받아볼 수 있기 때문에 그러한 기능이 필요한 서비스라면 캐시를 끄고 사용하는 것도 방법이 될 수 있다.

fetch 옵션에 따른 작동 방식

  • { cache: ‘force-cache’ } : 기본값으로, getStaticProps와 유사하게 불러온 데이터를 캐싱해 해당 데이터로 관리한다.
  • { cache: ‘no-store’ }: getServerSideProps와 유사하게 캐싱하지 않고 매번 새로운 데이터를 불러온다.
  • { next: { revalidate: 0 }} : getStaticProps에 revalidate를 추가한 것과 동일하며, 정해진 유효시간 동안에는 캐싱하고, 이 유효시간이 지나면 캐싱을 파기한다.

캐시와 mutating, revalidating

fetch의 기본 작동을 재정의하여, 해당 데이터의 유효한 시간을 정해두고 이 시간이 지나면 다시 데이터를 불러와서 페이지를 렌더링 하는 것이 가능하다.

// app/page.tsx

// 하위에 있는 모든 라우팅에서는 페이지를 1시간(3600초) 간격으로 갱신해 새로 렌더링 하게 된다.
export const revalidate = 3600; 

 

캐시의 갱신이 이루어지는 과정은 다음과 같다.

  • 최초로 해당 라우트로 요청이 올 때는 미리 정적으로 캐시 해 둔 데이터를 보여준다.
  • 캐시된 초기 요청은 revalidate에 선언된 값 만큼 유지된다.
  • 해당 시간이 지나도 일단 캐시된 데이터를 보여준다.
  • 캐시된 데이터를 보여주는 한 편, 시간이 경과했으므로 백그라운드에서 다시 데이터를 불러온다.
✔️ 새롭게 나온 기능들인 터보팩(beta), 서버 액션 등에 대해서 살펴보자

 

Next 13, 14 버전을 통해서 굉장히 새로운 기능들이 많이 등장을 하게 되었다. 그중에서는 Next팀에서 실험적으로 이런 기능을 만들어보면 어떨까? 하는 그러한 시도들도 많이 나타나고 있다. 보통 이러한 시도들은 알파 혹은 베타 버전으로 먼저 출시되는 경우가 많다. 이러한 버전의 기능들을 주의깊게 살펴본다면 정식으로 나왔을 때 좀 더 친숙하게 사용할 수가 있다. 하지만 아직은 안정된 버전이 아니기 때문에 프로덕션 환경에서 사용할 때는 조금 주의를 할 필요가 있다.

 

터보팩(beta)

  • 터보팩은 Rust로 작성된 JS, TS를 위한 번들러이다.
  • 기존의 Webpack을 대체하기 위해 만들어 졌다.
  • 현재는 개발 환경에서만 사용 가능하다.
  • 개발 서버 실행 시 -turbo 플래그를 사용
  • https://nextjs.org/docs/architecture/turbopack
{
  "scripts": {
    "dev": "next dev --turbo", // --turbo
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  }
}

아직 베타 버전이고 자바스크립트와 타입스크립트에 최적화된 증분하는 bundler이고 Rust로 작성이 되었고 Next.js 안에서 이제 만들어졌다. 페이지 및 앱 라우터 로컬환경에서는 사용할 수 있다.

 

지원하는 기능

  • 터보팩은 기본적으로 별도의 환경 설정을 할 필요가 없다. ⇒ 굉장히 가볍고 의존이 없기 때문에 다른 확정을 할 수 있는 가능성이 굉장히 열려있다.

지원하지 않는 기능

  • 개발 환경에서만 가능
  • Webpack을 대체하기 위해서 나온 기능이기 때문에 이제 Webpack의 환경 설정을 지원을 하고 있지 않다.
  • 터보팩은 SWC 컴파일러를 컴파일러의 모든 트랜스파일링 변환과 최적화를 이제 사용할 수 있기 때문에 즉, 레버리지에서 사용을 했기 때문에 Babel 역시 포함이 되지 않는다.
    • Babel은 타입스크립트를 자바스크립트로 트랜스파일링 하는 도구이다. 근데 그 도구를 대체하는 역할로 SWC가 이미 사용이 되었기 때문에 지원을 하고 있지 않는다.

서버 액션

서버 액션은 Next 13번에서는 실험 버전으로 나왔고 14 버전에서 정식 버전으로 나온 기능이다.

  • 서버 액션은 기본적으로 서버에서 실행되는 비동기 함수이다. 그리고 서버 컴포넌트와 클라이언트 컴포넌트에서 form을 제출하거나 데이터를 변경할 때 사용할 수 있는 도구이다.
  • 서버 액션은 API를 굳이 생성하지 않더라도 함수 수준에서 서버에 직접 접근해 데이터 요청 등을 수행할 수 있는 기능이다.
  • 서버 액션은 반드시 “use server” 지시자 안에서만 사용될 수 있다. 서버 컴포넌트와 클라이언트 컴포넌트에서 둘 다 사용이 가능하다. 단, 클라이언트 컴포넌트에서는 모듈 레벨에서 액션을 import 하는 것만 가능하다.
  • “use server”를 컴포넌트의 최상단에 넣던지 비동기 함수의 가장 상단에 넣어서 사용
  • 일반적으로 <form> 요소의 action 메서드에 의해 사용된다. 하지만 form 데이터 이외에도 useEffect나 이벤트 핸들러에도 함께 사용할 수 있다.
  • https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
// Server Component
export default function Page() {
  // Server Action
  async function create() {
    'use server'
 
    // ...
  }
 
  return (
    // ...
  )
}
  • 서버 컴포넌트에서는 인라인 레벨의 함수 그리고 모듈 레벨에서 'use server'라는 지시자를 포함을 해서 사용
  • 이 서버 액션을 안에다 넣기 위해서는 그 함수의 가장 최상단에 'use server' 지시자를 사용
  • 함수는 비동기 함수여야 한다.
'use server'
 
export async function create() {
  // ...
}
  • 클라이언트 컴포넌트에서는 모듈 레벨에서 import 할 때 사용할 수가 있고 'use server'라는 지시자를 포함해서 사용
  • 이 파일 안에서는 모든 함수들은 서버 액션으로 표시가 되고 클라이언트 컴포넌트와 서버 컴포넌트에서 재사용이 되는 함수를 만들 수가 있다.
import { create } from '@/app/actions'
 
export function Button() {
  return (
    // ...
  )
}

서버 액션을 클라이언트 컴포넌트의 props 형태로도 전달해서 사용할 수 있다.

<ClientComponent updateItem={updateItem} />
'use client'
 
export default function ClientComponent({ updateItem }) {
  return <form action={updateItem}>{/* ... */}</form>
}

행동

일반적으로 이 서버 액션이 가장 많이 사용되는 부분은 form 요소에서 액션 속성을 우리가 유발할 때 많이 사용된다.

  • 서버 컴포넌트가 기본적으로 점진적인 보강을 지원을 하는데 form이 자바스크립트에서 제출이 될 때 자바스크립트가 아직 로드가 되지 않거나 사용되지 않는 경우에도 form이 제출이 될 수 있도록 서버 컴포넌트에서는 지원을 하고 있다.
  • 클라이언트 컴포넌트에서는 form이 서버 액션을 유발을 해서 form 제출을 큐에다가 넣는 작업을 할 수가 있다. 이 역시 자바스크립트가 아직 로드가 되지 않거나 클라이언트 쪽에서 일어난 hydration 작업에 우선순위가 밀리더라도 form 제출을 할 수 있게 해준다.
  • hydration 작업이 다 되고 나서 브라우저는 form 제출을 refresh 하지 않게 해주는 기능도 있다.
  • 서버 액션은 이렇게 form에서만 국한되서 사용되는 것이 아니라 이벤트 핸들러라든지 useEffect 그리고 서드파티 도구들 버튼과 같은 다른 form 요소들에서도 사용이 될 수가 있다.

예시

Form

  • React는 HTML의 form 이라는 요소를 확장해서 쓸 수가 있는데 그 요소 안에서 서버 액션을 action props 안에서 사용할 수가 있다.
export default function Page() {
  async function createInvoice(formData: FormData) {
    'use server'
 
    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }
 
    // mutate data
    // revalidate cache
  }
 
  return <form action={createInvoice}>...</form>
}
  • 이 액션 속성에서는 자동으로 formData 오브젝트를 수신을 받게 된다. 이때 이 formData 데이터 안에 상태를 관리를 해야 되는데 이제는 더이상 useState 같은 상태관리 훅을 사용할 필요가 없다.
  • 대신에 formData에서 제공하는 메서드를 네이티브 메서드들을 추출해서 사용할 수가 있다.
  • createInvoice 함수 안에서 서버 액션을 만들어주고 안에서 formData를 설정 해주고 데이터를 수정해주고 revalidate해주는 역할까지 할 수 있다.
'use client'
 
import { updateUser } from './actions'
 
export function UserProfile({ userId }: { userId: string }) {
  const updateUserWithId = updateUser.bind(null, userId)
 
  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">Update User Name</button>
    </form>
  )
}
  • 클라이언트 컴포넌트에서는 추가적인 인자들을 bind 같은 메서드를 통해서 추가해 줄 수도 있다.
'use client'
 
import { useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button type="submit" disabled={pending}>
      Add
    </button>
  )
}
  • formData를 제출하기 전에 pending 되는 상태들이 있을 수가 있는데 useFormStatus라는 훅을 통해서 이 pending 상태를 제출하기 전에 pending 상태를 받아올 수가 있다. 그래서 이 상태를 form에서 버튼 같은 데에서 아직 제출이 되기 전이면 스피너 같은 걸 보여준다든지 아니면 비활성화를 시켜준다든지 중복되는 요청을 막을 수 있는 로직도 구현을 할 수가 있다.
'use server'
 
import { z } from 'zod'
 
const schema = z.object({
  email: z.string({
    invalid_type_error: 'Invalid Email',
  }),
})
 
export default async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
 
  // Return early if the form data is invalid
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
 
  // Mutate data
}
  • 아무래도 formData를 처리하면서 가장 신경을 많이 쓰는 부분 중의 하나가 validation 즉, 유효성 검증하는 부분이다.
  • 이러한 formData를 유효성 검증하고 또 에러를 처리하는 부분을 서버 사이드에서 해줄 수도 있다.
  • form 필드를 zod라는 라이브러리를 통해 수정을 하기 전에 검증을 할 수가 있다.

useFormState

'use client'
 
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
 
const initialState = {
  message: '',
}
 
export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)
 
  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
      <button>Sign up</button>
    </form>
  )
}
  • 필드가 서버 안에서 유효함이 입증이 됐을 때 서버는 직렬화된 오브젝트를 리액트 클라이언트 쪽으로 보내주게 된다. 리액트에서 useFormState로 사용자에게 메시지를 보여줄 수가 있게 된다.
  • createUser라는 서버 액션이 유저를 만드는 작업을 하면서 만약에 이메일이 유효하지 않았을 때 state의 메시지를 통해서 에러를 보여준다.

useOptimistic

'use client'
 
import { useOptimistic } from 'react'
import { send } from './actions'
 
type Message = {
  message: string
}
 
export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<
    Message[],
    string
  >(messages, (state, newMessage) => [...state, { message: newMessage }])
 
  return (
    <div>
      {optimisticMessages.map((m, k) => (
        <div key={k}>{m.message}</div>
      ))}
      <form
        action={async (formData: FormData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  )
}
  • 업테이트를 하는 과정에서도 UI를 업데이트할 때 굉장히 불필요한 상태 변경이라든지 렌더링이 일어날 수가 있는데 렌더링을 하는 부분을 최적화 해 줄 있는 훅이 바로 useOptimistic이다.
  • 응답을 기다리기보다는 서버 액션이 끝나기 전에 상태를 UI를 업데이트 해주는 과정에서 최적화를 해주는 훅이라고 보면 된다.

Non-form Elements

  • FormData를 사용할 때 뿐만 아니고 서버 액션은 formData가 아닌 경우에도 사용을 할 수가 있다

이벤트 핸들러

  • 이벤트 핸들러를 사용을 할 때 예를 들면, onClick을 사용을 할 때 서버 액션을 통해서 처리를 해 줄 수가 있다.
'use client'
 
import { incrementLike } from './actions'
import { useState } from 'react'
 
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)
 
  return (
    <>
      <p>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Like
      </button>
    </>
  )
}
  • onClick 안에서 incrementLike라는 서버 액션을 설정을 해줘서 likes의 업데이트를 해주는 방법을 설정을 해줄 수가 있다.
'use client'
 
import { publishPost, saveDraft } from './actions'
 
export default function EditPost() {
  return (
    <form action={publishPost}>
      <textarea
        name="content"
        onChange={async (e) => {
          await saveDraft(e.target.value)
        }}
      />
      <button type="submit">Publish</button>
    </form>
  )
}
  • 그리고 이 과정에서 아까 살펴봤었던 useOptimistic이나 useTransition을 통해서 UI 업데이트를 조금 최적화하는 방법도 고려를 해볼 수가 있다.
  • 이벤트 핸들러를 form 요소에 붙일 수도 있다. 값이 바뀔 때 마다 실행되는 onChange 핸들러의 서버 액션인 saveDraft를 호출해서 사용할 수가 있다.
  • 이런 경우에 여러 이벤트가 빠르게 연속적으로 발생할 수가 있다. 예를 들면, textarea의 값이 바뀌는 경우 서버 액션의 디바운스를 쓰는 걸 권장한다.
  • 왜냐하면 여러 이벤트가 동시에 발생되면서 상태들이 올바르게 업데이트가 안 될 수도 있다.

useEffect

  • useEffect를 사용할 때도 서버 액션으로 트리거를 해 줄 수가 있다.
  • 예를 들어 컴포넌트가 마운트가 될 때 그리고 의존성이 바뀔 때 변화들을 자동적으로 쫓아갈 수 있게 하는 과정에서 서버 액션이 도움이 될 수가 있다.
'use client'
 
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
 
export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)
 
  useEffect(() => {
    const updateViews = async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    }
 
    updateViews()
  }, [])
 
  return <p>Total Views: {views}</p>
}
  • initialViews 라는 값을 받고 incrementViews 라는 이제 뷰 값을 올려주는 서버 액션을 이 useEffect 안에서 비동기로 처리해 줄 수가 있다.
  • 그렇게 해서 상태를 업데이트 하면 incrementViews가 서버 쪽에서 로직이 수행이 되면서 상태가 업데이트가 된 뷰 값이 클라이언트에서 올바르게 보여진다.

 

 

이러한 요소들 외에도 에러를 처리하는 방법, 데이터를 다시 revalidate 하는 방법, redirecting 하는 방법 등등 이 서버 액션을 통해서 이런 이벤트를 처리하거나 데이터를 변경하는 과정에서 쓰이는 것 뿐만 아니라 그 이후에 하는 작업들에서 사용되는 경우가 많이 있다. 이러한 문법들은 공식문서를 읽어보면서 필요에 따라 사용해 보자.