Next.js는 Vercel이라는 미국 스타트업에서 만든 리액트 기반 서버 사이드 렌더링 프레임워크이다.
Next.js가 대세가 되기전 페이스북 팀에서 서버 사이드 렌더링을 위해 고려했던 프로젝트가 있는데 이게 바로 react-page이다. react-page는 페이지를 서버 또는 클라이언트에서 리액트를 손쉽게 사용할 수 있는 것을 목표로 만들어졌다.
물론 이 프로젝트는 현재 개발이 중지되었지만 여기서 구현해 둔 방향성에 Next.js가 영감을 받았다. 특히 Next.js의 페이지 구조가 이를 닮았다. 리액트 기반 프로젝트에서 서버 사이드 렌더링을 고려한다면 현재로는 Next.js를 선택하는 것이 가장 합리적인 선택일 수 있다.
Next.js 시작하기
CRA와 유사하게 next.js 에서는 명령어를 제공한다.
npx create-next-app@latest --ts
사실 13.x부터 등장한 Next.js에서 PageRouter 방식 대신에 AppRouter란 것이 있다.
14버전이 릴리즈되면서 아예 Next.js팀에서 AppRouter 방식을 권장하고는 있는데 이번 글에서는 PageRouter 방식에 대해 알아보고 AppRouter 방식은 추후에 또 알아보도록 하자.
package.json
페이지의 전반적인 정보들을 확인해볼 수 있다.
{
"name": "learn-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^18",
"react-dom": "^18",
"next": "14.0.4"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.0.4"
}
}
next.confing.js
Next.js 프로젝트의 환경설정을 담당한다.
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig
첫줄 주석은 js파일에 typeScript 타입 도움을 받기위해 추가된 코드이다.
책에서는 swcMinify : true 옵션도 넣어주는데 14버전 릴리즈 이후 아래와 같은 문구를 띄워주는걸 보면 아예 내장으로 옮길 예정으로 보인다.
Use SWC compiler to minify the generated JavaScript
@deprecated — will be enabled by default and removed in Next.js 15
@see — SWC Minification
page/_app.tsx
애플리케이션의 전체 페이지의 시작점으로 웹 애플리케이션에서 공통으로 설정해야 하는 것들을 여기서 실행할 수 있다.
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
1. 에러 바운더리를 사용한 에러 처리
2. reset.css와 같은 전역 css선언
3. 모든 페이지에 공통으로 사용 & 재사용해야 하는 데이터 제공
pages/_document.tsx
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body className="body">
<Main />
<NextScript />
</body>
</Html>
);
}
_document.tsx는 애플리케이션의 HTML을 초기화하는 곳이다.
1. <html> 이나 <body>에 DOM 속성을 추가하고 싶다면 해당 파일을 사용한다.
2. _app.tsx는 렌더링&라우팅에 따라 서버나 클라이언트에서 실행될 수 있지만 _document는 무조건 서버에서만 실행되며 따라서 이벤트 핸들러를 추가하는 것은 불가능하다.
3. CSS-in-JS의 스타일을 서버에서 모아 HTML로 제공
결국 _app.tsx파일은 Next.js를 초기화하는 파일로 Next.js 설정과 관련된 코드를 모아두는 곳이며 _document.tsx는 Next.js로 만드는 웹사이트의 뼈대가 되는 HTML설정과 관련된 코드를 추가하는 곳이라 이해하면 될 것 같다.
pages/_error.tsx
이 에러페이지를 만들면 클라이언트에서 발생하는 에러나 서버에서 사용하는 500에러를 처리할 수 있다.
import { NextPageContext } from "next";
function Error({ statusCode }: { statusCode: number }) {
return (
<>
{statusCode ? `서버에서 ${statusCode}` : "클라이언트에서"} 에러가
발생했습니다.
</>
);
}
Error.getInitialProps = ({ res, err }: NextPageContext) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : "";
return { statusCode };
};
export default Error;
개발 모드에서는 Next.js가 제공하는 개발자 에러 팝업이 나온다.
pages/404.tsx
404 페이지를 정의할 수 있다.
import { useCallback } from "react";
export default function My404Page() {
const handleClick = useCallback(() => {
console.log("hi"); // eslint-disable-line no-console
}, []);
return (
<h1>
페이지를 찾을 수 없습니다. <button onClick={handleClick}>클릭</button>
</h1>
);
}
pages/500.tsx
마찬가지로 500에러를 핸덜링할 수 있다.
import { useCallback } from 'react'
export default function My500Page() {
const handleClick = useCallback(() => {
console.log('hi') // eslint-disable-line no-console
}, [])
return (
<h1>
(500페이지) 서버에서 에러가 발생했습니다.{' '}
<button onClick={handleClick}>클릭</button>
</h1>
)
}
404와 500 파일은 없으면 next.js에서 제공한 페이지를 볼 수 있다.
pages/index.tsx
해당 영역부터는 개발자가 자유롭게 명칭을 지정하여 만들 수 있는 페이지이다.
먼저 간단한 페이지의 구조를 보자.
index.tsx : 웹사이트의 루트
hello.tsx : /hello가 주소가 되며 해당 주소랑 연결되는 파일
hello/world.tsx : /hello/world 로 접근되는 주소
hello/[greeting].tsx : [ ] 안에는 어떠한 문자도 올 수 있다는 뜻이며 들어오는 문자가 greeting이라는 변수값으로 들어온다.
hi/[...props].tsx : hi 하위의 모든 주소가 들어온다.
localhost:3000/hi/hello , localhost:3000/hi/hello/world/foo 등 모든 url을 받으며 props 변수로 들어온다.
Next.js는 폴더 구조가 라우팅이 되는 매우 직관적인 구조를 가지고 있음을 알 수 있다.
hi/[...props].tsx
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { NextPageContext } from 'next'
export default function HiAll({ props: serverProps }: { props: string[] }) {
const {
query: { props },
} = useRouter()
useEffect(() => {
/* eslint-disable no-console */
console.log(props)
console.log(JSON.stringify(props) === JSON.stringify(serverProps)) // true
/* eslint-enable no-console */
}, [props, serverProps])
return (
<>
hi{' '}
<ul>
{serverProps.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</>
)
}
export const getServerSideProps = (context: NextPageContext) => {
const {
query: { props },
} = context
return {
props: {
props,
},
}
}
위와같은 방법으로 props를 가져올 수 있다.
서버 라우팅 vs 클라이언트 라우팅
Next.js는 서버 사이드 렌더링을 지원하지만 동시해 SPA과 같이 클라이언트 라우팅 또한 수행한다. 이 방식이 혼재되어 있어 처음에는 혼란을 느낄 수 있다.
먼저 Next.js는 분명한 서버 사이드 렌더링 프레임워크다. 페이지의 루트 컴포넌트에 console을 찍어서 확인해보자.
분명 서버에서 실행되는 것을 확인 할 수 있다.
이번에는 index.tsx를 아래와 같이 수정해보자.
import type { NextPage } from 'next'
import Link from 'next/link'
const Home: NextPage = () => {
return (
<ul>
<li>
{/* next의 eslint 룰을 잠시 끄기 위해 추가했다. */}
{/* eslint-disable-next-line */}
<a href="/hello">A 태그로 이동</a>
</li>
<li>
{/* 차이를 극적으로 보여주기 위해 해당 페이지의 리소스를 미리 가져오는 prefetch를 잠시 꺼두었다. */}
<Link prefetch={false} href="/hello">
next/link로 이동
</Link>
</li>
</ul>
)
}
export default Home
next/link는 Next.js에서 제공하는 라우팅 컴포넌트인데 <a/> 태그와 비슷한 동작을 한다.
언뜻보면 두 버튼은 똑같이 보이지만 엄밀히 다르다. A태그는 잠시 깜박인 이후에 페이지 라우팅을 하지만 next/link를 활용한 이동은 SPA처럼 이동한다.
A 태그는 모든 리소스를 처음부터 받는 반면 next/link는 페이지 이동에 필요한 내용만 받는 것을 볼 수 있다.
next.link를 받는 경우 네트워크에 hello.js만 받고있다. 해당 파일을 한번 보자.
(self.webpackChunk_N_E = self.webpackChunk_N_E || []).push([[628], {
3737: function(n, u, t) {
(window.__NEXT_P = window.__NEXT_P || []).push(["/hello", function() {
return t(2488)
}
])
},
2488: function(n, u, t) {
"use strict";
t.r(u),
t.d(u, {
__N_SSP: function() {
return o
},
default: function() {
return r
}
});
var e = t(5893)
, o = !0;
function r() {
return console.log("클라이언트"),
(0,
e.jsx)(e.Fragment, {
children: "hello"
})
}
}
}, function(n) {
n.O(0, [774, 888, 179], function() {
return n(n.s = 3737)
}),
_N_E = n.O()
}
]);
물론!! 난독화되어 있어서 알아보긴 힘들지만 우리가 넣은 console.log가 있다는 것을 확인할 수 있는데 next/link로 이동하는 경우 클라이언트에서 필요한 js 파일만 불러온 뒤 라우팅하고 있다는 점을 알 수 있다.
즉 Next.js는 서버 사이드 렌더링의 장점 즉 사용자가 빠르게 볼 수 있는 최초 페이지를 제공한다는 점과 싱글 페이지 애플리케이션의 장점인 자연스로운 라우팅이라는 두 가지 장점을 모두 살리고 있다.
이러한 Next.js의 장점을 살리기 위해선 아래와 같은 규칙을 지켜야 한다.
1. <a> 대신 <Link> 사용
2. window.location.push 대신 router.push 사용
getServerSideProps
뭔가 hello 예제에서 getServerSideProps는 아무 동작도 하지 않는것처럼 보인다.
export default function Hello() {
console.log(typeof window === 'undefined' ? '서버' : '클라이언트') // eslint-disable-line no-console
return <>hello</>
}
// export const getServerSideProps = () => {
// return {
// props: {},
// }
// }
해당 함수가 있는상태 & 없는상태로 각각 빌드를 해보자.
서버 사이드 런타임 체크가 되어있다.
빌드 크기가 약간 줄며 서버 사이드 렌더링이 필요 없는 정적인 페이지로 분류되었다.
즉 getServerSidProps가 없으면 서버 사이드 렌더링이 필요없는, 빌드 시점에 미리 만들어도 되는 페이지로 간주해버린다. 따라서 페이지에 접속해도 아까와 같은 '서버' 라는 문장을 출력하지 않는다.
api/hello.ts
서버의 api를 정의하는 폴더이다. 해당 주소는 다른 pages파일과는 다르게 HTML요청을 하는 것이 아닌 서버 요청을 받는다.
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
interface Data {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
res.status(200).json({ name: 'John Doe' })
}
이곳의 코드는 서버에서만 실행되며 일반적인 프론트 프로젝트를 만든다면 작성할 일이 거의 없다.
하지만 서버에서 내려주는 데이터를 조합하여 BFF(backend-fro-frontend) 형태를 활용하거나 풀스택 애플리케이션을 구축하는 경우, 혹은 CORS 문제를 우회하는 경우 사용할 수 있다.
Data Fetching
Next.js에서는 서버 사이드 렌더링 지원을 위한 몇 가지 데이터 불러오기 전략이 있는데 이를 Data Fetching이라고 한다.
ㅎ당 함수는 pages/ 폴더에 있는 라우팅이 되는 파일에서만 사용할 수 있으며 반드시 정해진 함수명으로 export를 사용하여 함수를 파일 외부로 내보내야 한다.
getStaticPaths & getStaticProps
어떠한 페이지를 CMS(Contents Managemen System)이나 블로그,게시판과 같은 정적으로 결정된 페이지를 보여주고자 하는 경우 사용할 수 있는 함수이다.
만약 pages/post/[id] 와 같은 페이지가 있다고 생각해보자.
import { GetStaticPaths, GetStaticProps } from 'next'
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
fallback: false,
}
}
export const getStaticProps: GetStaticProps = async ({ params }: any) => {
const { id } = params
const post = await fetchPost(id)
return {
props: { post },
}
}
export default function Post({ post }: { post: Post }) {
//post로 페이지를 렌더링
}
getStaticPaths는 pages/post/[id] 가 접근하는 주소를 정의한다. 해당 페이지는 post/1 과 post/2만 접근가능하게 해주며 그외 페이지는 404를 반환한다.
getStaticPRops는 해당 페이지로 요청이 온 경우 제공할 props를 반환한다.
즉, 해당 함수들을 활용하면 빌드 시점에 미리 데이터를 불러온 다음 정적인 HTML 페이지를 만들 수 있다.
사용자가 접근할 수 있는 페이지를 모조리 빌드해두고 배포하기 때문에 사용자는 굳이 페이지가 렌더링되는 것을 기다릴 필요가 없게 된다.
getStaticPaths의 fallback옵션은 빌드해야할 페이지가 너무 많은 경우 사용할 수 있다. paths에 미리 빌드해둘 몇개의 페이지만 리스트로 반환하고 true나 "blocking:으로 선언하면 된다.
이경우 next build를 실행할때 미리 반환해둔 paths에 기재된 페이지만 미리 빌드해놓고 나머지는 아래와 같이 로딩 페이지와 같은 처리를 할 수 있다.
// fallback : true
export default function Post({ post }: { post: Post }) {
const router = useRouter()
if (router.isFallback) {
return <div>...loading</div>
}
//post 렌더링
}
만약 "blocking"으로 처리를 한 경우 사용자가 그저 기다리게 할수도 있다.
getServerSideProps
이 함수가 있다면 무조건 페이지 진입 전에 해당 함수를 실행한다. 해당 함수의 응답값에 따라 페이지의 루트 컴포넌트에 props를 반환하거나 다른 페이지로 redirect 시키는것 또한 가능하다.
해당 함수가 존재한다면 Next.js는 해당 페이지를 서버에서 꼭 실행해야 하는 페이지로 분류한다.
export default function Post({ post }: { post: Post }) {
//post 렌더링
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const {
query: { id = '' },
} = context
const post = await fetchPost(id.toString())
return {
props: { post },
}
}
만약 위와같이 getServerSideProps가 존재한다면 페이지의 Post컴포넌트에 해당 값을 제공하여 이값을 기준으로 렌더링을 수행할 수 있다.
또한 아까 hello 예제에서 본것과 같이 아래와 같이 빌드파일이 구성된다.
<script id="__NEXT_DATA__" type="application/json">
{
"props": { "pageProps": {} },
"page": "/hello",
"query": {},
"buildId": "IwlfrjyZ0xzuLiPFEW44B",
"nextExport": true,
"autoExport": true,
"isFallback": false,
"scriptLoader": []
}
</script>
그럼 왜 __NEXT_DATA__라는 id가 지정된 script가 추가되어 있을까?
리액트의 서버 사이드 렌더링을 하는 작동을 다시 생각해보자.
1. 서버에서 fetch등으로 렌더링에 필요한 정보를 가져옴
2. 가져온 정보로 HTML 작성
3. 2번 정보를 브라우저에 제공
4. 3번 정보로 hydrate작업
5. hydrate작업 결과물과 서버의 HTML이 다르다면 불일치 에러를 뱉음
6. 5번작업 역시 fetch 등을 활용해야함
즉 1~6번 작업 사이의 fetch 시점에 따라 결과의 불일치가 일어날 수 있는데 따라서 1번 정보를 script형태로 저장하는 것이다. 이러면 1번의 작업을 6번에서 반복하지 않을 수 있다.
즉 getSErverSideProps의 props로 내려줄 수 있는 값은 JSON으로 제공할 수 있는 값으로 제한된다는 점을 알아두자.
부가적으로 생각할 부분은 아래와 같다.
1. window.documnet와 같이 브라우저에서 접근할 수 있는 객체에는 접근 X
2. API호출시 완전한 주소를 제공해야 함
3. 해당 함수에서 에러 발생시 500페이지로 리다이랙트
항상 getServerSideProps는 서버에서 실행되는 함수라는 사실을 기억해두자. 따라서 이 실행이 끝나기전까지는 사용자에게 어떤 HTML도 보여줄 수 없기 때문에 최대한 간결하게 작성하고 꼭 최초에 보여줘야 하는 정보가 아니라면 해당 함수보다 클라이언트에서 호출하는 것이 더 유리할 수 있다.
또한 조건에 따라서 다른 페이지를 보내는 기능 또한 redirect를 사용할 수 있다.
export const getServerSideProps: GetServerSideProps = async (context) => {
const {
query: { id = '' },
} = context
const post = await fetchPost(id.toString())
if(!post){
redirect : {
destination : '/404'
}
}
return {
props: { post },
}
}
해당 함수는 Next.js에서 서버 사이드 렌더링을 잘 표현하기 위한 핵심 함수이다!!! 잘기억하자.
getInitialProps
getStaticProps나 getSErverSidePRops가 나오기 전에 사용할 수 있었던 유일한 페이지 데이터 불러오기 수단이었으며 대부분 경우에는 방금 말한 함수들을 사용하는것이 권장된다.
단 과거의 작성된 코드나 _app.tsx와 같은 일부에서 getInitialProps밖에 사용할 수 없으니 알아두도록 하자 ( ㅠ ㅠ ;)
getInitialProps를 하나 작성해보겠다! pages/todo/[id].tsx 파일을 하나 만들자.
import Link from 'next/link'
import { NextPageContext } from 'next'
export default function Todo({
todo,
}: {
todo: { userId: number; id: number; title: string; completed: boolean }
}) {
return (
<>
<h1>{todo.title}</h1>
<ul>
<li>
<Link href="/todo/1">1번</Link>
</li>
<li>
<Link href="/todo/2">2번</Link>
</li>
<li>
<Link href="/todo/3">3번</Link>
</li>
</ul>
</>
)
}
Todo.getInitialProps = async (ctx: NextPageContext) => {
const {
query: { id = '' },
// asPath,
// query,
// res,
} = ctx
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${id}`,
)
const result = await response.json()
return { todo: result }
}
이전에 본 함수들과 가장 큰 차이점은 페이지의 루트 함수에 정적 메서드로 추가한다는 점과 props 객체를 반환하는것이 아닌 객체르 ㄹ바로 반환한다는 점이다.
스타일 적용하기
전역 스타일
_app.tsx에 필요한 스타일을 import로 불러오면 애플리 케이션 전체에 영향을 미칠 수 있다.
import type { AppProps } from 'next/app'
import '../styles/globals.css'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
컴포넌트 레벨 CSS
[name].module.css 와같은 명명 규칙을 준수한다면 컴포넌트 레벨의 CSS를 쉽게 추가할 수 있다.
이 CSS는 다른 컴포넌트의 클래스명과 겹쳐서 스타일에 충돌이 일어나지 않는 고유의 클래스명을 제공한다.
예를들어
// button.modules.css
.alert {
color : red;
}
위와같은 컴포넌트를 Button.tsx파일에서 만들었다면 실제 빌드를 한경우
.Button_alert__32TJN
위와같이 클래스가 조금 다르게 바뀌게 된다.
SCSS & SASS
scss & sass 역시 css와 동일한 방식으로 사용할 수 있다.
CSS-in-JS
해당방식이 코드작성의 편의성 이외 성능이점 이나 여러 부분에서는 논쟁거리가 있긴 하지만 분명 CSS구문이 JS내부에 있어서 직관적으로 편리하게 느껴질 수 있는 방식이다.
여러가지 CSS-in-JS라이브러리들 또한 Next.js에서 사용할 수 있다.
https://nextjs.org/docs/app/building-your-application/styling/css-in-js
지원하는 스타일들과 적용방법은 위 공식문서를 참고하면 좋을 것 같다.
_app.tsx 응용
_app.tsx가 Next.js로 만든 서비스가 진입하는 최초 진입점이란 것을 안다면 사용자가 맨 처음 서비스에 접근했을때 하고 싶은 무언가를 여기서 처리하는 것이 가능하다.
먼저 _app.tsx에 getInitialProps를 추가해보자. 이경우 아래와같이 추가해야한다.
import App, { AppContext } from 'next/app'
import type { AppProps } from 'next/app'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
MyApp.getInitialProps = async (context: AppContext) => {
const appProps = await App.getInitialProps(context)
return appProps
}
await App.getInitialProps(context) 코드가 없다면 다른 페이지의 getInitialProps가 정상실행되지 않는다.
이 부분을 아래와 같이 바꾸고 실행결과를 잘보자.
import App, { AppContext } from 'next/app'
import type { AppProps } from 'next/app'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
MyApp.getInitialProps = async (context: AppContext) => {
const appProps = await App.getInitialProps(context)
const isServer = Boolean(context.ctx.req)
console.log(`
[${isServer ? '서버' : '클라이언트'}] ${context.router.pathname}에서 ${
context.ctx?.req?.url
}을 요청함.
`)
return appProps
}
해당 코드가 포함된 애플리케이션의 실행절차는 아래와 같다.
1. 자체페이지의 getInitialProps방문
2. getServerSideProps가 있는 페이지를 <Link>로 방문
3. 1번 페이지를 <Link>로 방문
4. 2번 페이지를 <Link>로 방문
Link를 활용하면 라우팅은 클라이언트 렌더링처럼 작동한다. 페이지 방문 최초시점에는 서버 사이드 렌더링이 전체적으로 작동해야하기때문에 페이지 전체를 요청하지만 그 이후에는 클라이언트 라우팅을 수행하기 위해서 getServerSideProps결과를 json파일만을 요청해서 가져온다.
클라이언트 렌더링이 일어나는 경우에는 서버 로그가 뜨지않고 브라우저에서도 콘솔이 남아있는 걸 확인할 수 있다.
즉 웹서비스에 최초에 접근했을때만 실행하고 싶은 내용을 app.getInitialProps내부에 담아두는 것이 가능하다.
userAgent나 사용자 정보와 같은 애플리케이션 전역에 걸쳐 사용해야 하는 정보 호출등의 작업을 넣어둔다면 편할 것이다.
next.confing.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig
basePath : URL을 위한 기본 주소값
만약 해당값을 docs로 둔다면 localhost:3000/docs 가 기본 URL이 된다.
powerdByHeader
Next.js는 응답관련 헤더에 X-Power-by : Next.js정보를 제공하는데 이 정보를 없앨 수 있다. 보안관련 솔루션에서는 해당 부분을 취약점으로 분류하니 false로 해두자.
redirects
특정 주소를 다른 주소로 보내고 싶을때 가능, 정규식도 사용이 가능해서 응용이 가능하다.
지금까지 아주 간략하게(?) Next.js를 다뤄봤다. 사실 Next.js가 단순히 폴더 구조가 짜여진 CRA이며 서버사이드 렌더링을 지원한다.. 정도만 알고있었는데 이정도 깊은 원리가 있는지 깨닫게 되서 재밌었다.
끄읏 -!
'FrontEnd > Deep Dive' 카테고리의 다른 글
[React] Deep Dive 모던 리액트(16) 훅을 통한 전역상태관리 구현 (0) | 2024.01.03 |
---|---|
[React] Deep Dive 모던 리액트(15) 리액트와 상태관리 라이브러리 역사 (0) | 2024.01.02 |
[React] Deep Dive 모던 리액트(13) 리액트와 서버사이드 (1) | 2023.12.27 |
[React] Deep Dive 모던 리액트(12) 서버사이드 렌더링(SSR) (0) | 2023.12.26 |
[React] Deep Dive 모던 리액트(11) 사용자 정의 훅 & 고차 컴포넌트 (0) | 2023.12.24 |