Migration: CRA To Next.js (feat. React v18)
- #React
- #CRA
- #SSR
- #NextJS
- #migration
- #SEO
์๋
ํ์ธ์. ์๋์ ํ๋ก ํธ์๋ํ ํฉํธ์ฐฌ์
๋๋ค.
๊ธฐ์กด ์๋์ ํ๋ก ํธ์๋ ํ๋ก์ ํธ๋ CSR(CRA) ๋ฐฉ์์ ์น ํ์ด์ง์์ต๋๋ค.
์ต์ด ์์ฑ ์์๋ ๋น ๋ฅธ ๊ฐ๋ฐ ์งํ์ ์ํด์, ๊ทธ ํ์๋ ๋ง์ด๊ทธ๋ ์ด์
์ ๊ธฐํ ๋น์ฉ์ด ๋ฎ์์ ๋ฑ์ ์ด์ ๋ก SSR(Next.js) ๋์
์ ๋ฏธ๋ฃจ๊ณ ์์์ต๋๋ค.
์ด๋ฒ ๊ธ์์๋ ๊ทธ๋ฐ ํ๋ก ํธ์๋ ์๋์ ์น ํ๋ก์ ํธ์ ์ Next.js๋ฅผ ๋์
ํ๊ณ , ๋ฌด์์ด ๋ฌ๋ผ์ก๋์ง ์ด์ผ๊ธฐํด๋ณด๋ ค๊ณ ํฉ๋๋ค.
Why?
์, ๊ทธ๋ผ ์ฌํ๊ป CSR๋ก ๋ฐฉ์น(?)ํด๋์ ํ๋ก์ ํธ์ ๊ฐ์๊ธฐ ์ Next.js ๋์ ์ ์๋ํ๊ฒ ๋์์๊น์?
์ฒซ ๋ฒ์งธ๋ก๋, ์ฐ์ ๊ฐ๋จํ SEO ์ฒ๋ฆฌ์
๋๋ค.
๋ฐ์์ ์ค๋ช
ํ ์์ ์ด์ง๋ง, CSR์์์ ๋ณต์กํ SEO์ฒ๋ฆฌ๋ฅผ Next.js์์๋ ๊ฐ๋จํ ์ฒ๋ฆฌํ ์ ์๋ค๊ณ ์๊ฐํ์ต๋๋ค.
๋ ๋ฒ์งธ ์ด์ ๋ ๋ฐ๋ก React 18๋ฒ์ ์ ์ ์ ์ถ์๋ก ์ธํ SSR์์์ Suspense ์ง์์ด์์ต๋๋ค.
SSR์์์ Suspense๋ฅผ ์ ์ ์ง์ํ๊ฒ ๋จ์ผ๋ก์จ, SSR์ด ๊ฐ๋ ๋ช๊ฐ์ง ๋ฌธ์ ์ ๋ค์ด ํด์๋์์ต๋๋ค.
์ด๋ Next.js๋ก์ ๋ง์ด๊ทธ๋ ์ด์ ์ ์งํํ๊ธฐ์ ์ถฉ๋ถํ ์ด์ ์๊ณ , ์ด๋ฒ ๊ธฐํ์ Next.js ์ ์ฉ๊ณผ ํจ๊ป React18 ์ ๋ฐ์ดํธ ์์ ์ ์งํํ๊ฒ ๋์์ต๋๋ค.
๊ทธ๋์ ๋ญ๊ฐ ๋ฌ๋ผ์ง๊ธด ํ๋?
1. SEO ์ฒ๋ฆฌ
CSR ๊ธฐ๋ฐ์ ์น์ ๊ธฐ๋ณธ์ ์ผ๋ก SEO ์ฒ๋ฆฌ๊ฐ ๋ถ๊ฐ๋ฅํฉ๋๋ค. CRA๋ก ๋ง๋ ํ๋ก์ ํธ์์ ์น ํฌ๋กค๋ฌ๊ฐ body ํ๊ทธ๋ด๋ถ์์ ์ฐพ์๋ณผ์ ์๋๊ฑดโฆ
<div id="root"></div>
์ค์ ๋ด์ฉ๋ค์ด ๋ ๋๋ง๋ root ๋ ธ๋ ํ์ค ๋ฟ์ด๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ ๊ธฐ์กด ํ๋ก์ ํธ์์๋ AWS CloudFront Lambda@Egde๋ฅผ ํตํด์ SEO ์ฒ๋ฆฌ๋ฅผ ํ๊ณ ์์์ต๋๋ค.
ํ์ด์ง๊ฐ ๋ณ๊ฒฝ๋๊ฑฐ๋ ์์ฑ๋๋ฉด Lambda@Edge ์ฝ๋์ ํด๋น ํ์ด์ง์ ๋ํ SEO ์ฒ๋ฆฌ๋ฅผ ์ถ๊ฐ/์ญ์ ํ๊ณ ๋ณ๊ฒฝ๋ ์ฝ๋๋ฅผ ๋ค์ CloudFront์ ๋ฐฐํฌํ๋ ๊ณผ์ ์ ๊ฑฐ์ณ์ผ ํ์ต๋๋ค.
let html = await new S3({signatureVersion: 'v4'})
.getObject({
Bucket: stageVariable.Bucket,
Key: 'index.html'
})
.promise()
.then(value => value.Body.toString());
// html head meta ์์
response.body = html;
return response;
ํ์ง๋ง Next.js๋ฅผ ๋์ ํ ํ์๋ ๊ฐ ํ์ด์ง์์ getStaticProps ํน์ getServerSideProps์์ SEO ์ฒ๋ฆฌ๋ฅผ ์ํ ๊ฐ์ ์ ์ํ๊ณ , ํด๋น ๊ฐ์ ํ์ด์ง์ props๋ก ๋๊ธฐ๊ธฐ๋ง ํ๋ฉด ๋ณ๋ ๋ฐฐํฌ ์์ด๋ ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
export const getServerSideProps = async ({query}) => {
const response = await getClass({classId: Number(query.id)});
if (!response) {
return {
redirect: {
permanent: false,
destination: Path.Class
}
};
}
return {
props: {
meta: {
title: response.title
}
}
};
};
<title>{props.meta.title}</title>;
์ด์ฒ๋ผ ์ค์ ํ ๋ด์ฉ์ด ์ ์์ ์ผ๋ก html์ ํฌํจ๋๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
2. ํด๋ ๊ตฌ์กฐ ๋ณ๊ฒฝ
๊ธฐ์กด ํ๋ก์ ํธ์ ํด๋ ๊ตฌ์กฐ๋ ๊ฐ ํ์ด์ง์ ํด๋นํ๋ ํด๋๋ฅผ ๋ง๋ค๊ณ , ๊ทธ ํด๋๊ฐ ํด๋น ํ์ด์ง์์ ์ฌ์ฉํ๋ ๋ชจ๋ ์์๋ฅผ ํฌํจํ๊ณ ์๋ ๊ตฌ์กฐ์์ต๋๋ค.
src/pages/Home
- index.tsx
- styles.ts
- types.ts
- components
- HomeBanner
- index.tsx
- HomeContents
- index.tsx
- ...
ํ์ง๋ง Next.js์ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ pages ํด๋์ ํ์ ํด๋ ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ผ์ฐํ
์ ํ๋ ํ์์ผ๋ก, ๊ธฐ์กด์ ํด๋ ๊ตฌ์กฐ๋ฅผ ์ ์งํ ์ ์์์ต๋๋ค.
์ด๋ก ์ธํด ํด๋ ๊ตฌ์กฐ ์์ฒด๋ฅผ ๋ณ๊ฒฝํ๊ฒ ๋์๊ณ ๊ธฐ์กด์ ํ์ด์ง๋ณ๋ก ๋ด๋ถ ์์๋ฅผ ๊ด๋ฆฌํ๋ ๊ตฌ์กฐ๊ฐ ์๋ ๊ฐ ์์ ๋ด๋ถ์์ ํ์ด์ง ๋ณ๋ก ๊ด๋ฆฌํ๋ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝํ๊ฒ ๋์์ต๋๋ค.
src/
- pages/home
- components/home
- ...
3. react-router์์ next/router๋ก์ ๋ณ๊ฒฝ
CRA ๊ธฐ๋ฐ์ ๊ธฐ์กด ํ๋ก์ ํธ์์๋ history ๊ด๋ฆฌ๋ฅผ ์ํด์ react-router ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
ํ์ง๋ง Next.js์์๋ ์์ฒด์ ์ผ๋ก next/router๋ฅผ ์ง์ํ์ฌ, next/router๋ก์ ์์ ์์
์ ์งํํ์์ต๋๋ค.
๋ ์ฌ์ด์ ๋ฌธ๋ฒ๊ณผ ๊ธฐ์กด์ ์ง์ํ๋ ๊ธฐ๋ฅ ์ค, ์ง์ํ์ง ์๋ ๊ธฐ๋ฅ์ด ๋ช๊ฐ ์์๊ณ
ํนํ history ๊ฐ์ฒด์์์ ๋ช๋ช ๋ณ์ ๋ฐ ๋ฉ์๋๋ค์ next/router์์ ์ฌ์ฉ๋ฒ์ด ๋ค๋ฅด๊ฑฐ๋ ์๋ ๊ธฐ๋ฅ์ด์ด์
next/router์ ์ ์ ํ ๊ธฐ๋ฅ๋ค๋ก ๋์ฒดํ์ฌ ์ฒ๋ฆฌํด์ผ ํ์ต๋๋ค.
// v1
history.push('/class');
history.push('/class', {param: 'value'});
history.state; // {param: value}
if (history.action === 'pop');
// v2
router.push('/class');
router.push({pathname: '/class', query: {param: 'value'}});
history.query; // {param: 'value'}
router.beforePopState(() => {});
4. Suspense with SSR
๊ธฐ์กด์ SSR์ ํด๋ผ์ด์ธํธ์์ HTML์ ๋ฐ์์ ๋ ๋๋ง์ ์งํํ๊ณ , ํ์ด๋๋ ์ด์
์ ์งํํฉ๋๋ค.
์ด ๊ธฐ๋ฅ๋ง์ผ๋ก๋ ์ถฉ๋ถํด ๋ณด์ผ์ ์์ง๋ง, ๋ฌธ์ ๋ ์ ํ๋ฆฌ์ผ์ด์
์ ์ฒด๋ฅผ ๋์์ผ๋ก ์ฒ๋ฆฌ๋๋ค๋ ๊ฒ์
๋๋ค.
๋ฐ์ดํฐ๊ฐ ์์๋๋ง ์ค์ ์ปดํฌ๋ํธ๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ๋ฐ์ดํฐ ํจ์นญ์ด ์๋ฃ๋์ง ์์๋, fallbackUI๋ฅผ ๋
ธ์ถํ๊ณ ์ถ์๋, ๋ฐ์ดํฐ์ ์กด์ฌ ์ฌ๋ถ๋ฅผ ํ์
ํ์ฌ ์กฐ๊ฑด๋ถ ๋ ๋๋ง ์ฒ๋ฆฌ๋ฅผ ์งํํ๊ณ ์์์ต๋๋ค.
ํ์ง๋ง ์ด๋ ๋ฐ์ดํฐ, Javascript, ํ์ด๋๋ ์ด์
์ ๊ณผ์ ์ ๋ชจ๋ ๊ธฐ๋ค๋ฆฐ ํ์์ผ ๋น๋ก์ ์ํธ์์ฉ์ด ๊ฐ๋ฅํ๋ค๋ ์ด์๊ฐ ์์ต๋๋ค.
- Container, Component1, Component2 ๋ ๋๋ง
- Container, Component1, Component2 ํ์ด๋๋ ์ด์
- ์ฌ์ฉ์ ์ํธ์์ฉ ๊ฐ๋ฅ
//index.tsx
const data = fetch('url');
return (
<Container>
<Component1 />
{data && <Component2 props={data} />}
</Container>
)
ํ์ง๋ง React18์์ SSR์ Suspense๋ฅผ ์ง์ํ๊ฒ ๋๋ฉด์ ์ ๋ฌธ์ ์ ๋ค์ ํด๊ฒฐํ ์ ์์์ต๋๋ค.
- Container, Component1, Component2 fallbackUI ๋ ๋๋ง
- Container, Component1 hydration => Container, Component ์ฌ์ฉ์ ์ํธ์์ฉ ๊ฐ๋ฅ
+) Component2 ๋ ๋๋ง, Component2 ํ์ด๋๋ ์ด์
// Component2.tsx
const data = useSWR('url', {suspense: true, revalidateOnMount: true});
// index.tsx
return (
<Container>
<Component1 />
<Suspense fallback={<Loading />}>
<Component2 />
</Suspense>
</Container>
)
๊ธฐ์กด์ rendering, ํ์ด๋๋ ์ด์
์ ๋ชจ๋ ๊ธฐ๋ค๋ฆฌ๊ณ ๋์์ผ User ์ํธ์์ฉ์ด ๊ฐ๋ฅํ๋๊ฒ๊ณผ ๋ฌ๋ฆฌ
2๋ฒ ์์ ์ ์ฌ์ฉ์๊ฐ Component2 ์์ญ์ ์ก์
์ ๋ฐ์์ํค๋ฉด 2๋ฒ ๊ณผ์ ์ ๊ฑฐ์น์ง ์๊ณ Component2์ ๋ ๋๋ง, ํ์ด๋๋ ์ด์
์ ์ฐ์ ์ ์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค.
๋ํ Component2์ ํ์ด๋๋ ์ด์
์ด ์๋ฃ๋์ง ์์์ด๋, ๋ค๋ฅธ ์ปดํฌ๋ํธ์ ๋ํ ์ํธ์์ฉ์ด ๊ฐ๋ฅํฉ๋๋ค.
์ด ์ธ์๋ ์๋ฒ ์ฌ์ด๋์์ window ๊ฐ์ฒด์ ๋ถ์ฌ๋ก ์ธํ ์ด์ ์ฒ๋ฆฌ, Layout ์ปดํฌ๋ํธ ๊ด๋ฆฌ ๋ฑ ์๋ง์ ์ด์ ํด๊ฒฐ ๋ฐ ๋ณ๊ฒฝ์ด ์์์ง๋ง ๊ฐ์ฅ ํฌ๊ฒ ๋ฌ๋ผ์ง ๋ถ๋ถ์ ์ด๋ ๊ฒ ๋ค๊ฐ์ง๋ผ๊ณ ์๊ฐํฉ๋๋ค.
์ด์ ๋จ์์ผ์โฆ
์ด๋ ๊ฒ CRA ๊ธฐ๋ฐ CSR ํ๋ก์ ํธ(v1)์์ Next.js ๊ธฐ๋ฐ์ SSR ํ๋ก์ ํธ(v2)๋ก์ ์๋์ ํ๋ก ํธ์๋ ๋ง์ด๊ทธ๋ ์ด์
์ ๋๋ฌ์ต๋๋ค!!!
ํ์ง๋ง ์์ง ๋ถ์กฑํ ๋ถ๋ถ, ์์ ํด์ผํ ๋ถ๋ถ์ด ์ฐ๋๋ฏธ์
๋๋ค.
์๋์ ์น์ ๊พธ์คํ ์
๋ฐ์ดํธ ๋๊ณ ์๊ณ , ๋ง์ด๊ทธ๋ ์ด์
์ ํ๋๋ฐ ์์ด์ ์
๋ฐ์ดํธ๋๊ณ ์๋ ๋ชจ๋ ์์๊ฐ ํฌํจ๋์ด์ผ ํฉ๋๋ค.
์ด์, ํ ๋ด๋ถ์ ์ผ๋ก ๋
ผ์ํ์ฌ ์ต์ํ์ผ๋ก ๋ฐฐํฌํ ์ ์๋ ์์ค์ ์ฝ๋์ธ ์ํ๋ก ๋ฐฐํฌ ํ, v2์์ ์ถ๊ฐ ๊ฐ๋ฐ์ ์งํ ๋ฐ ๊พธ์คํ ๋ฆฌํฉํ ๋ง์ ์งํํ๊ธฐ๋ก ํ์ต๋๋ค.
์์ผ๋ก v2 ๋ฒ์ ์ ์๋์ ์น์์๋ ์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐ ๊ฐ๋ฐ๊ณผ ํจ๊ป
๋์ฑ SSR ๋ค์ด, ๋์ฑ React๋ค์ด ์ฝ๋๋ก์ ๋ฆฌํฉํ ๋ง์ ์งํํ ์์ ์
๋๋ค.
๋ค์ ๊ธ์์๋ v2 migration ์งํ ์, AWS S3 (CSR) ์์ AWS Serverless (SSR) ๋ก์ ๋ฌด์ค๋จ ๋ฐฐํฌ์
AWS Serverless์์ Vercel๋ก์ ๋ฐฐํฌ ํ๋ซํผ ๋ณ๊ฒฝ์ ๋ํ ๋ด์ฉ์ ๋ค๋ฃจ์ด๋ณด๋ ค๊ณ ํฉ๋๋ค. ๋ง์ ๊ด์ฌ ๋ถํ๋๋ฆฝ๋๋ค!
๊ฐ์ฌํฉ๋๋ค.