<Who's the next? 2탄 nextjs>

강민수·2023년 9월 21일

1. 지난 편에 이어서...

지난 편에서는 rsc에가 무엇이며, 썼을 때의 이점 등을 살펴봤다. 그래서 이번 편에서는 어떻게 적용이 실제되고 있는 지 살펴볼 예정이다.

이번 예제는 참고로 필자가 기존에 작업해 둔 next.js로 작성해 둔 연습용 blog 코드를 토대로 설명한다.

코드를 살펴보기에 앞서, next.js가 13버전과 RSC의 연관성부터 살펴보자.

2. next 13은 뭐가 달라졌나?

next 13 버전에 들어오면서 가장 큰 변화는 단언코 App-Router다.

1) 어딘지 모를 찝찝함.

next를 기존에 써 본 사용자라면, 어느 정도 공감할 수 있는 부분이다. 이게 막상 써보면, react와 사실 뭐가 다른 지 명확하게 느끼기 어렵다.

물론, 그도 그럴 것이 react기반 프레임워크지만, 기본적으로 모든 컴포넌트들은 client와 server를 구분짓지 않는다.

즉, 코드로 살펴보면, 아래와 같았다.

export default function Page({ data }) {
  // Render data...
}
 
// This gets called on every request
export async function getServerSideProps() {
  // Fetch data from external API
  const res = await fetch(`https://.../data`)
  const data = await res.json()
 
  // Pass data to the page via props
  return { props: { data } }
}

기존에는, 특정 페이지 컴포넌트에서 ssr, ssg 등을 활용하고자 했다면, getServerSideProps 혹은 getStaticProps 등을 활용했다. 그리고 저 함수들 안에서만 next 서버 단의 코드나 활용이 가능했다. 나머지 클라이언트 코드는 기존에 리액트 컴포넌트와 동일하게 사용했다.

물론, 리액트를 접한 사람 입장에서는 훨씬 빠르게 이해하기 쉬운 접근이기는 했지만, 어딘가 모르게 조금 찝찝한 구석은 여전히 존재했다.

  1. 서버와 독립된 컴포넌트로 관리될 수는 없나?
  2. 정말로 이게 서버사이드 랜더링인 것인가?
  3. 항상 ssr, ssg 등은 매번 함수를 저렇게 설정하는 게 귀찮다.

이런 부분을 해소하는 차원에서 next13이 나왔다.

2) Server Components

next 13에서는 새로운 Server Components라는 아키택처를 제공해 준다.

이전 편을 자세히 살펴보신 분들이라면, 어느 정도 눈치 채셨을 것이다. 그렇다.

바로. 저 Server Components는 리액트 RSC의 개념을 가지고 만든 것이다.

리액트 18에서 설명한 RSC와 큰 차이는 없다. 간략히만 소개하자면, 다음과 같다.

1. 기본 서버 컴포넌트 적용.

이제 next13부터는 기존에 사용하던 getServerSideProps라던지, getStaticProps는 사용할 필요가 없다.

왜냐면, 기본적으로 모든 페이지 컴포넌트 서버 컴포넌트기 때문이다.
이게 무슨 소리냐면, 결국 기본적으로 모든 코드는 서버단에서 실행된다는 얘기다. 그외에 나머지 클라이언트 단 코드들은 클라이언트 컴포넌트에서 별도로 분리되어 실행된다. 즉, rsc처럼 서버와 클라이언트가 할 일을 명확하게 구분하는 것이다.

next 공식문서에 따르면 둘의 차이를 아래처럼 정리해 놓았다.

즉, 기본적인 use~ 관련된 훅스들은 전부 서버 단에서는 사용이 불가하다. 또한, 그 외에 랜더링 이후 모든 상호 작용들(onClick, onChange 등) 역시 사용할 수 없다. 이는 오직, 클라이언트 컴포넌트에서만 가능하다.

서버는 단지, 데이터를 받아오거나 db에 대한 접근이나 여러 가지 클라이언트와 상호 작용이 불 필요한 것들을 처리할 수 있다.

조금 더 자세한 코드 차이는 이후에 필자가 예시로 만들어 놓은 블로그 코드를 통해 더 이해해 보자.

3) streaming의 변화.

1. 살펴보기.

두 번째로 살펴볼 변화는 streaming 방식의 변화다.

기존 ssr의 플로우는 아래 그림과 같다.

  1. Server(API)로 부터 Data를 가져옴

  2. Server(Next.js)에서 HTML을 렌더링 함

  3. Client(browser)에서 코드(HTML)를 받음

  4. JavaScript를 받아와 Hydrating함

이 과정이 모두 끝나기 전까지 유저는 페이지와 상호작용을 할 수 없다.

그렇다면 이 문제를 어떻게 해결해야 할까..?

여기서 바로 제안되는 방법이 Streaming이다.

이미지처럼 HTML을 작게 나누어 모든 데이터가 로드되기 전 준비된 컴포넌트는 미리 완성해 상호작용할 수 있게 해주는 기술을 Streaming이라고 한다.

이 기술을 사용해 우선순위가 높은 컴포넌트를 먼저 작동하게 해 줄 수 있다.

말만으로 이해하기는 어려우니 사진을 함께 첨부해 보면,

위와 같이 컴포넌트 단위대로 따로 로드하고 렌더링 하게 되어 맨 위 컴포넌트의 TTI는 매우 단축된 것을 알 수 있다.

2. 활용 기술.

1) loading.js


이와 같이 app아래 loading.js(loading.tsx)를 만들게 되면

layout.js, page.js를 포함한 아래 jsx파일들은 전부 Suspense로 래핑 되게 되어 loading 될 때까지 loading.js이 반환하는 Element를 렌더링 하게 된다.

2) suspense.

이 방법은 익숙한 사람도 있을 것 같다.

아래와 같이 비동기 컴포넌트를 Suspense로 감싸고 Suspense 내부에 fallback에 로딩바나 스피너와 같은 로딩 컴포넌트를 넣는 방식이다.

function Page() {
   return (
   	<div>
           <Suspense fallback={<Loading />}>
             <FetchingComponent />
           </Suspense>
       </div>
   )
}

3. 코드를 비교해 보면서 이해 해보자.

위의 내용을 그렇다면, 기존에 서버 컴포넌트와 어떻게 다른 지 살펴보자.

아래는 필자가, 기존에 next12버전으로 만든 블로그 예제 코드다.

1. 기존 코드

import styled from '@emotion/styled';
import Link from 'next/link';
import Post from '../types/common/post';
import { getAllPosts } from './api/api';
type Props = {
    allPosts: Post[];
};

const Container = styled.div`
    display: flex;
    width: 100%;
    height: 100vh;
    background: black;
    display: flex;
    flex-direction: column;
    gap: 50px;
    padding: 50px;
`;

const Home = ({ allPosts }: Props) => {
    return (
        <Container>
            <h1>All Posts</h1>
            {allPosts.map((value) => (
                <Link as={`/${value.slug}`} href={`/[id]`}>
                    <div>{value.title}</div>
                </Link>
            ))}
        </Container>
    );
};

export default Home;

export const getStaticProps = async () => {
    const allPosts = getAllPosts(['title', 'date', 'slug', 'author', 'coverImage', 'excerpt']);
    return {
        props: { allPosts },
    };
};

기존 코드는 보는 것처럼 getStaticProps와 클라이언트가 함께 하나의 컴포넌트에 적혀있다.

그렇다면 아래 바뀐 코드는 어떻게 다를까?

2. 변경된 코드.

//page.tsx

import { getAllPosts } from '../pages/api/api';
import PostList from './home-page';

const getPostData = async () => {
    const allPosts = getAllPosts(['title', 'date', 'slug', 'author', 'coverImage', 'excerpt']);
    return allPosts;
};

const Home = async () => {
    const postingData = await getPostData();
    return <PostList allPosts={postingData} />;
};
export default Home;
// home-page.tsx
'use client';
import styled from '@emotion/styled';
import Link from 'next/link';
import { Items } from '../pages/api/api';
import Post from '../types/common/post';
type Props = {
    allPosts: Post[] | Items[];
};

const Container = styled.div`
    display: flex;
    flex-direction: column;
    gap: 15px;
    div {
        cursor: pointer;
    }
`;

const PostList = ({ allPosts }: Props) => {
    return (
        <Container>
            <h1>All Posts</h1>
            {allPosts.map((value) => (
                <Link as={`/${value.slug}`} href={`/[id]`} key={value.slug}>
                    <div>{value.title}</div>
                </Link>
            ))}
        </Container>
    );
};

export default PostList;

이처럼 코드가 두개로 나뉘었다. 바로 위에가 server-component이고 아래가 client-component다.

가장 큰 특징은 아까도 설명했듯이, 기존에 ssr, ssg에서 쓰이던 get~ 이후 함수는 사라졌다. 이는 간단하게 설명하자면, next13에서는 기본적으로 서버 컴포넌트는 다음과 같이 기본적으로 data-fetching을 통해 간단히 구현할 수 있다.

// 직접 무효화 하기 전까지는 이 request는 캐싱됨.
// `getStaticProps`와 비슷! (즉, 빌드 시점에 fetch)
// `force-cache`가 디폴트 값이므로 생략 가능
fetch(URL, { cache: 'force-cache' });

// 매번 요청 때마다 refetch 됨.
// `getServerSideProps`와 비슷!
fetch(URL, { cache: 'no-store' });

// 이 request는 10초동안 캐싱됨.
// This request should be cached with a lifetime of 10 seconds.
// `revalidate` 옵션을 지정한 `getStaticProps`와 비슷!
fetch(URL, { next: { revalidate: 10 } });

클라이언트 코드는 'use-client'라고 컴포넌트 상단에 적어줘야 한다. 이를 통해 기존의 react에서 사용하던 hooks와 client 상호작용 코드들을 사용할 수 있다.

참고로, 최상단의 클라이언트 컴포넌트에 use-client 선언을 하면, 안의 중첩된 컴포넌트들은 굳이 선언하지 않아도 된다.

6. 마무리.

이번 시간에는 next13 버전이 어떻게 react18의 rsc를 적용시켜서 변화했는 지 살펴봤다. 이를 토대로 리액트 팀이 어떤 방향성으로 자신들의 철학을 관철시켜서 클라이언트 개발 시장을 구축해 나가고 있는 지 알 수 있었다. 또한, 관련된 프레임워크 중에서도 대표격인 next 역시 그에 발맞춰 발전하고 있는 모습을 볼 수 있었다.

하지만, 여기서 한 가지 아직 우려할만한 점은 분명있다.

리액트 18버전이 나온지, 이제 1년이나 지났지만, 아직도 무수히 많은 기존 시장에서는 rsc 도입을 적극적으로 할 수는 없다.

모든 기술에는 얻는 게 있다면 잃는 게 있는 만큼... rsc 적용을 통해 우리가 얻는 이점은 분명히 크다. 하지만, 아직 다양한 라이브러리들에서 적용하지 못한다. 대표적으로 next의 경우, 대다수가 사용하는 ui 프레이워크인 emotion은 아직 공식적으로 서버 컴포넌트 적용이 안 되고 있다. 그로 인해, 기존에 emotion 스타일로 작업해 둔, 작업자들은 간만 보고 있는 실정이다. 이외에도 점점 업데이트는 되고 있지만 아직은 잘 모르겠다는 것이 현재의 결론이다.

모든 게 때가 되면 분명 적용이 될 것이며, 우리 프론트엔드 개발자는 이 기세를 계속 주시하며 언젠가는 바뀌게 될 시장의 판도를 읽는 눈이 중요한 것이 아닐까라고 생각한다.

ps. 리액트 쿼리쪽이나 다른 프레임워크들 역시 계속 메이저 업데이트가 있는 것으로 알고 있는데. 앞으로 어떻게 변할 지 계속 주시하며 포스팅도 함께할 예정이니 참고 바람.

profile
개발도 예능처럼 재미지게~

0개의 댓글