본문 바로가기
Resource/웹 프론트엔드

[Frontend] Next.js server action + CSR

by 우창욱 2024. 10. 19.

개요

Next.js 의 장점인 서버 사이드 렌더링을 최대한으로 활용하려면, server action 이 필요하게 됩니다. 그렇지만 서버 사이드 렌더링만을 사용해서는 인터렉티브 한 웹사이트를 만들려고 할 때 ‘use client'를 쓰라는 오류를 만나게 됩니다.

결국 Next.js를 잘 쓰려면 서버 사이드 렌더링클라이언트 사이드 렌더링을 잘 접목시켜야 하는데 이번 SARDIP work-space 프로젝트에서는 이 작업을 어떻게 진행했는지 설명드리겠습니다.

Server Action

Next.js 공식 문서에서는 server action을 서버에서 실행되는 비동기 함수라고 정의하고 있습니다. 서버 및 클라이언트 컴포넌트에서 호출된다고 하는데 개인적으로는 보통 클라이언트 사이드에서 사용하는 경우가 많았던 것 같습니다.

아래 코드처럼 컴포넌트 내부에서 use server 로 사용하는 것은 서버 컴포넌트에서만 가능합니다. 서버 컴포넌트에서의 server action은 이미 서버에 존재하기 때문에 서버에서 실행되고 HTML을 스트리밍된 응답으로 반환합니다.

 
// Server Components에서 사용되는 server action

export default function Page() {
  // Server Action
  async function create() {
    'use server'
    // Mutate data
  }
 
  return '...'
}

다들 놀리던 이 컴포넌트는 결국 서버에서 동작하는 거라서 클라이언트 / 브라우저에서 볼 수 없답니다.

 

클라이언트 컴포넌트에서 사용하는 경우, action 파일을 따로 작성하게 됩니다. 클라이언트 컴포넌트에서 사용된 server action은 Next.js에 의해 기본적으로 POST 요청으로 변환됩니다.

클라이언트 사이드에서 사용된 server action은 Next.js 내부 백엔드 서버로 요청이 전송되기 때문에 외부에선 이 요청 라우팅을 알 수가 없어서 보안적인 이점이 있다고 할 수 있겠습니다.

'use server';

export async function deleteImageGroup(
  imageGroupId: string,
  categoryId: string,
  accessToken: string,
) {
  const myDataService = new MyDataService(accessToken);
  try {
    await myDataService.deleteImageGroupById(imageGroupId, categoryId);
  } catch (error) {
    if (error instanceof Error) {
      return {
        errors: {
          _form: [error.message],
        },
      };
    } else {
      return {
        errors: {
          _form: ['Failed to delete Image Group'],
        },
      };
    }
  }
  revalidatePath('/work-space/my-data');
}

본론

이번 SARDIP Beta-2의 마지막 스프린트에서는 사용자 워크스페이스 프로젝트를 개발했습니다. 개인 사용자마다 할당되는 페이지이기 때문에 기존 SARDIP Platform 프로젝트보다는 동적인 요소가 적었습니다. 데이터 렌더링 하는 것도 크게 변동이 없는 구조였기 때문에 서버 사이드 렌더링으로 구현했습니다.

SARDIP /work-space 프로젝트에서의 데이터 패칭 및 렌더링 예시
MyDataCards 컴포넌트는 서버 컴포넌트입니다.

그렇지만 항상 상황이 즐겁게 풀리지 만은 않더군요. 저는 SARDIP의 사이드바를 간과하고 있었습니다.

???: 두 번째 사이드바에서 렌더링 되는 데이터는 첫 번째 사이드바에서 가져온 데이터의 id 값으로 요청하는 데이터니까~ /work-space/my-data/[postId] 같은 패턴으로 불러오면 되겠지~~

 

라고 생각 했지만 두 번째 사이드바는 클라이언트 컴포넌트로 렌더링할 수 밖에 없는 UI 였으며 각종 편법이 통하지 않는 컴포넌트였습니다. 왜냐면 인터렉티브하게 동작이 되어야 하는 사이드바였기 때문입니다.

 

인터렉티브한 UI를 조심하세요

 

엥? 첫 번째 사이드바도 두 번째 사이드바처럼 인터렉티브하게 동작하는 데 왜 서버 사이드 렌더링이 되었나요? 라고 물으실거 같아서, 첫 번째 사이드바는 각 페이지에 바인딩된 컴포넌트여서 가능했다고 말씀드릴 수 있겠습니다. 좀 더 자세히 설명드리면, Next.js의 page / layout.tsx 컴포넌트에서 props로 받을 수 있는 children을 이용했기 때문에, url에 따라 다른 API를 호출을 하고 서버에서 HTML을 렌더링 해줄 수 있는 첫 번째 사이드바는 비교적 쉽게 해결할 수 있었던 것입니다.

서버에서 받아온 스트림 HTML인 children만을 렌더링하도록 해서 문제를 해결함

 

무튼 그런 상황에서 서버와 클라이언트 컴포지션 패턴을 쓰면 해결되지 않을까? 하는 생각을 가지고 이것저것 적용하러 뛰어들었는데, 지금까지의 제 경험으로는 불가능했습니다.

클라이언트 컴포넌트가 되어버리면, 그 아래에서 하는 모든 것들은 자동으로 클라이언트 컴포넌트가 된다는것을 가볍게 생각했습니다. 진짜 모든 컴포넌트들을 다 클라이언트 컴포넌트로 바꿉니다. 항상 잊지 말고 개발하시길 바랍니다… 시간을 아껴줍니다.

결국 두 번째 사이드바는 클라이언트 렌더링을 해줘야 겠네… 하고 클라이언트 사이드 렌더링을 진행했습니다.


착착 개발을 진행해가고 있었는데 데이터를 추가하거나 삭제, 이동할 때 기존 데이터를 revalidate 하는 로직이 필요하다는 것을 깨달았습니다. 문제는 서버 사이드에서 렌더링 한 데이터도 revalidate를 해주어야 하고, 클라이언트 사이드에서 렌더링한 데이터도 revalidate를 해주어야 한다는 것이었습니다.

 

다행히도 Next.js에서는 form 엘리먼트 만이 아니라, useEffect나 이벤트 핸들러, 버튼에도 적용이 가능하다는 점이었습니다. 그래서 따로 비동기 함수를 정의를 하고, 비동기 함수 내부에 server action을 호출하고, 클라이언트 데이터도 revalidate 로직을 호출하는 식으로 문제를 해결하였습니다.

결론 및 참고 사항

server action 이라는 강력한 기능이 Next.js에 제공되어서 프론트엔드 개발자는 Node.js 서버를 구동하거나 백엔드 개발자에게 요청할 필요가 없이 서버 사이드 로직과, 렌더링을 쉽고 빠르게 개발할 수 있게 되었습니다.

하지만, 아시다시피 프론트엔드 개발자는 클라이언트와 브라우저 환경이라는 곳에서 결코 자유로울 수가 없습니다. 인터렉티브한 화면을 찍어내야 하니까요. 이는 프론트엔드만의 특징이라는 점도 존재하지만 한편으로는 갑갑한 짐이기도 합니다. 그렇지만 프론트엔드 개발을 시작한 이상, 도망칠 수는 없습니다.

 

앞으로 Next.js를 사용해서 개발을 진행하실 때, 클라이언트 사이드 컴포넌트를 사용하는 것을 최소화 하는 습관을 들인다면 어느정도 개발의 편의성을 확보할 수 있을 것이라고 생각합니다.

이런식으로요-1
이런식으로요-2

 

또한 백엔드 지식의 확장으로 더 넓은 분야에 도전해볼 수 있는 경험으로도 활용할 수 있을 것이라고 생각합니다. 그냥 갑자기 든 생각이라 신뢰성은 0이지만, 지금의 시대는 여러 분야에 흩어진 개발자를 하나의 개발자의 형태로 만들어가고 있는게 아닌가 하는 생각도 듭니다.