React의 Hydration에 대하여
Hydration
웹페이지를 렌더링하는 과정에서 React와 같은 CSR라이브러리는 번들된 js를 가져와 DOM을 렌더링합니다. 또한 상황에 따라 SSR을 선택하곤 합니다.
개발자들은 Server Side단에서 먼저 정적 페이지를 렌더링하고 JS파일들도 번들링한 후에 둘다 Client Side로 보내주는 생각을 했습니다. 하지만 DOM에는 동적인 이벤트가 하나도 없는 메마를 상태일 것입니다. 그래서 이 메마른 뼈대에 수분을 보충해서, 즉 HTML코드와 JS코드를 서로 매칭시켜 동적인 웹사이트를 브라우저에 렌더링하는 기술이 등장했습니다.
ReactDOM.render(element, container, [callback]);
이는 특정 컴포넌트를 두 번째 파라미터인 지정된 DOM요소에 하위로 주입하여 렌더링을 처리해주는 함수입니다.
그리고 렌더링이 완료되면 특정 이벤트를 처리할 콜백 함수를 세 번쨰 파라미터로 넣어줄 수 있습니다.
ReactDOM.hydrate(element, container, [callback]);
이는 특정 컴포넌트를 두 번쨰 파라미터인 지정된 DOM요소에 하위로 hydrate처리만 합니다. 이는 렌더링을 통해 새로운 웹 페이지를 구성할 DOM을 생성하는 것이 아니라, 기존 DOM Tree에서 해당되는 DOM요소를 찾아 정해진 자바스크립트 속성(이벤트 리스너 등)들만 부착시키겠다는 말입니다.
ISR
기존의 CSR방식을 사용하면 그 안의 데이터를 불러오는 데는 시간이 좀 걸립니다. 그 반면에 SSR을 사용하면, 유저가 페이지와 데이터를 한 번에 볼 수 있다는 점입니다. 하지만 페이지를 불러오는 게 오래 걸리면 유저한테 아무 것도 안보이기 때문에 안 좋을 수도 있습니다.
또한 SSG를 알아보았는데, 이건 정적 HTML을 미리 생성해주기 때문에 JS코드를 다운받을 필요가 없습니다. 하지만 이의 문제는 getStaticProps함수가 빌드 타임 때 한 번만 실행된다는 점입니다. 이 때문에 페이지 안의 데이터를 변경하려면 페이지 전체를 다시 빌드해야 한다는 단점이 있습니다.
그렇다면 ISR을 사용한다면 페이지에 로딩 상태가 전혀 나타나지도 않고, 서버단에서 페이지를 렌더링해주지 않아도 됍니다. 그리고 getServerSideProps를 사용하지 않아도 되고 페이지도 즉시 불러올 수 있게 됩니다. 유저가 페이지를 열면 서버단에서 렌더링되는게 아닙니다. 유저가 페이지를 열면 이미 렌더링된 상태고 로딩 상태도 표시되지 않지만 표시되는 데이터는 가장 최신 데이터가 될 겁니다.
https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration
공식문서를 보면, with out needing to rebuild the entire site라고, 빌드를 모든 페이지를 하지 않고 최신상태로 그 페이지만 빌드하게 도와줍니다.
이를 구현한 코드를 봅시다.
/pages/community/index.tsx
import type { NextPage } from "next";
import Link from "next/link";
import Layout from "@components/layout";
import FloatingButton from "@components/FloatingButton";
import useSWR from "swr";
import { Post, User } from "@prisma/client";
import useCoords from "@libs/client/useCoords";
import client from "@libs/client/client";
interface PostWithUser extends Post {
user: User;
_count: {
wondering: number;
answers: number;
};
}
interface PostsResponse {
ok: boolean;
posts: PostWithUser[];
}
const Community: NextPage<PostsResponse> = ({ posts }) => {
// const { latitude, longitude } = useCoords();
// const { data } = useSWR<PostsResponse>(
// latitude && longitude
// ? `/api/posts?latitude=${latitude}&longitude=${longitude}`
// : null
// );
return (
<Layout title="동네 생활" hasTabBar>
<div className="space-y-8 px-4 py-2">
{posts.map((post) => (
<Link key={post.id} href={`/community/${post.id}`}>
<a className="flex cursor-pointer flex-col items-start pt-3">
<span className="flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800">
동네질문
</span>
<div className="mt-2 text-gray-700">
<span className="font-medium text-orange-500">
Q.
</span>{" "}
{post?.question}
</div>
<div className="mt-5 flex w-full items-center justify-between text-xs font-medium text-gray-500">
<span>{post?.user?.name}</span>
<span>{post?.createdAt as any}</span>
</div>
<div className="mt-3 flex w-full space-x-5 border-t border-b-[2px] py-2.5 text-gray-700">
<div className="flex items-center space-x-2 text-sm">
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
{/* <div>궁금해요 {post._count.wondering}</div> */}
</div>
<div className="flex items-center space-x-2 text-sm">
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
></path>
</svg>
<div>답변 {post._count?.answers}</div>
</div>
</div>
</a>
</Link>
))}
<FloatingButton href="/community/write">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
></path>
</svg>
</FloatingButton>
</div>
</Layout>
);
};
export async function getStaticProps() {
const posts = await client.post.findMany({
include: {
user: true,
},
});
return {
props: {
posts: JSON.parse(JSON.stringify(posts)),
},
revalidate: 20,
};
}
export default Community;
이렇게 되어 있는데, 만약에 revalidate가 된다면 serverless function이 실행되어 페이지를 리빌드 된 결과를 반환하게 되는 원리라고 합니다. 이를 구현하는 것은 getStaticProps의 반환값에 revalidate라는 옵션에 초단위 값을 넣어주면 되는데, 최소한의 시간을 넣어주면 됩니다. 만약 10을 넣었다면, 최소한 10초는 되고 사용자로부터 요청이 들어오면 리빌딩이 일어나 Static Page를 갱신한다는 점입니다.
getServerSideProps는 여러번 실행되지만, getStaticProps는 ISR을 사용하면 몇 번이든 실행시킬 수 있습니다. 그리고 NextJS가 그 새로 생성된 HTML을 유저에게 전달하는 플로우 입니다. 유저는 로딩 상태를 안 봐도 되지만 동시에 가장 최신 데이터도 볼 수 있게 됩니다.
그리고 함수 안에 console을 찍어보고 build타임에 한번만 실행되는지 확인해 보도록 하겠습니다.
빌드가 정확히 잘 되는 것을 알 수 있습니다.
다음과 같이 정상적으로 Good testing이라는 이름으로 쓴 질문이 revalidate되는 것을 확인할 수 있었습니다.
이 방식은 HTMl안에 데이터가 이미 들어있기 때문에 로딩 없이 바로 데이터를 볼 수 있습니다. 하지만 유저가 보는 데이터가 가장 최신 데이터는 아니고 20초 정도 전의 데이터를 보게 됩니다. 하지만 유저가 사이트에 방문하지 않으면 HTML이 20초, 40초, 60초마다 재생성되는게 아닙니다.
'Web > NextJs' 카테고리의 다른 글
[ Next.js - DeepDive ] - Blocking SSG (0) | 2022.08.12 |
---|---|
[ Next.js - DeepDive ] - Incremental site regeneration - 2 ( On-Demand ) (0) | 2022.08.12 |
[ Next.js - DeepDive ] - Dynamic getStaticProps (0) | 2022.08.06 |
[ Next.js - DeepDive ] - Dynamic getStaticProps (0) | 2022.08.06 |
[ Next.js - DeepDive ] - getStaticProps part Two (0) | 2022.08.06 |