Next.js는 Vercel이라는 미국 스타트업에서 만든 풀스택 웹 애플리케이션을 구축하기 위한 리액트 기반 프레임워크다.
Next.js가 웹 프론트에서 대세가 되기에 앞서 페이스북 팀에서 SSR을 위해 진행한 react-page 프로젝트가 있었다. 해당 프로젝트는 페이지를 서버 또는 클라이언트에서 리액트를 쉽게 사용할 수 있는 것을 목표로 만들어졌다.
물론 해당 프로젝트는 개발 중단되었지만, 구현된 방향성에 Next.js가 영감을 받았다. 특히 Next.js의 페이지 구조가 이를 닮았다.
만약 이 글을 읽고 있고 리액트 기반의 프로젝트에서 SSR을 고려하고 있다면 현재로서는 Next.js을 선택하는 것이 가장 합리적인 선택일 수 있다.
Next.js 시작하기
Next.js는 CRA와 비슷한 creact-next-app을 제공한다.
// npm or npx
npm(npx) create-next-app@latest --ts
// yarn
yarn create next-app@latest --ts
Introduction: App Router | Next.js
The App Router is a file-system based router that uses React's latest features such as Server Components, Suspense, Server Functions, and more.
nextjs.org
Next.js 팀에서는 14 버전 부터 기존의 PageRouter 방식 보다는 13 버전에 릴리스된 AppRouter 방식을 권장하고 있다.
이 글에서는 학습을 위해 PageRouter를 사용해 예제를 작성했다.
package.json
프로젝트 구동에 필요한 모든 명령어 및 의존성이 포함된 정보를 확인할 수 있다.
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.3.3"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.3.3",
"@eslint/eslintrc": "^3"
}
}
next.config.js
Next.js 프로젝트의 환경 설정을 담당하며 Next.js를 잘 사용하기 위해 잘 알아야 하는 파일이다.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
reactStrictMode: true,
};
export default nextConfig;
import 뒤에 type이 붙은 이유는 JS 파일에서 TS의 타입 도움을 받기 위해 추가된 코드이다.
교제에서는 swcMinify: true 라는 코드를 작성했는데, 이는 최신 버전의 Next.js에서는 프레임워크 내부에서 기본적으로 true 값을 갖도록 수정되었기에 따로 수정하지 않아도 된다.
pages/_app.tsx
애플리케이션의 전체 페이지의 시작지점으로 웹 애플리케이션에서 공통으로 설정해야 하는 것들이 이 곳에서 실행할 수 있다.
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
- 에러 바운더리를 사용해 애플리케이션 전역에서 발생하는 에러 처리
- reset.css와 같은 전역 css 선언
- 모든 페이지에 공통으로 사용 또는 제공해야하는 데이터 제공
간단하게 최초에는 SSR을, 이후에는 CSR 방식으로 _app.tsx가 렌더링 된다고 생각하자.
pages/_document.tsx
_document.tsx는 create-next-app으로 생성했다면 없다.
없어도 되는 파일이다.
해당 파일은 애플리케이션의 HTML을 초기화 하는 곳이다.
- <html>이나 <body>에 DOM 속성을 추가하고 싶을 때 사용한다.
- _app.tsx는 렌더링이나 라우터에 따라 서버나 클라이언트에서 실행되며 hydrate를 실행할 수 있지만, _document.tsx는 무조건 서버에서 실행되기에 이벤트 핸들러 추가와 같은 동작은 불가능하다.
- CSS-in-JS의 스타일을 서버에서 모아 HTML로 제공한다.
- ❌ getServerSideProps, getStaticProps 등의 서버에서 사용 가능한 데이터 불러오기 함수는 해당 파일에서 사용할 수 없다.
_app.tsx와 _document.tsx의 차이점
차이점 | _app.tsx | _document.tsx |
용도 | Next.js를 초기화 및 설정과 관련 코드를 모아둠 | HTML 설정과 관련된 코드 추가 |
렌더링 영역 | 서버와 클라이언트 모두 | 서버 한정 |
pages/_error.tsx
이 파일은 필요에 따라 생성해야 하며, 클라이언트에서 발생하는 에러 또는 서버에서 발생하는 500 에러를 처리할 수 있다.
import { NextPageContext } from 'next';
const Error = ({ statusCode }: { statusCode?: number }) => {
return (
<p>
{statusCode ? `서버에서 ${statusCode}` : '클라이언트에서'} 에러가 발생했습니다
</p>
);
}
Error.getInitialProps = ({ res, err }: NextPageContext) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : '';
return { statusCode };
};
export default Error;
개발자 모드에서는 해당 페이지에 방문할 수 없지만, Next.js에서 제공하는 에러 팝업이 나타난다.
pages/404.tsx
404 에러에 대응하는 404 페이지를 정의할 수 있는 파일이다.
const My404Page = () => {
return <h1>페이지를 찾을 수 없습니다.</h1>
}
export default My404Page;
따로 해당 파일을 만들지 않는다면, Next.js에서 제공하는 페이지가 적용된다.
참고로 PageRouter에서는 모든 에러는 _error.tsx, 404 에러는 404.tsx, 500 에러는 500.tsx가 해준다.
AppRouter에서는 런타임 에러는 error.tsx, 404 에러는 not-found.tsx가 해준다.
이때 Next.js의 런타임은 서버와 클라이언트 양쪽에 존재하는 코드를 실행해 페이지를 렌더링하는 모든 과정을 의미한다.
pages/500.tsx
마찬가지로 500 에러를 핸덜링하는 페이지이다.
const My500Page = () => {
return <h1>서버에서 에러가 발생했습니다.</h1>
}
export default My500Page;
이 파일 또한 추가로 만들 수 있지만 만들지 않아도 Next.js에서 제공하는 기본 페이지를 볼 수 있다.
pages/index.tsx
지금 부터는 개발자 마음대로 명칭을 지정해 만들 수 있는 페이지이다.
Next.js의 라우팅 구조는 /pages 디렉터리를 기초로 구성되며, 각 페이지에 있는 default export로 내보내진 함수가 페이지의 루트 컴포넌트가 된다.
- index.tsx : 웹 사이트의 루트
- hello.tsx : /hello가 주소가 되며 해당 주소와 연결되는 파일
- hi/[...props].tsx : hi 하위의 모든 주소가 들어오며 [...props] 부분은 hi/hello/world/foo 등이 props라는 변수에 ["hello", "world" "foo"] 형식의 배열로 들어온다.
hi/[...props].tsx
import { NextPageContext } from 'next';
import { useRouter } from 'next/router'
import { useEffect } from 'react';
const HiAll = ({ props: serverProps }: { props: string[] }) => {
const { query: { props } } = useRouter();
useEffect(() => {
console.log(props);
console.log(JSON.stringify(props) === JSON.stringify(serverProps));
}, [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
}
}
}
export default HiAll;
위 코드와 같이 useRouter를 통해 query에 있는 props 객체에 접근해 정보를 얻을 수 있다.
서버 라우팅과 클라이언트 라우팅의 차이
Next.js는 서버 사이드 렌더링을 수행하지만 클라이언트 SPA와 같이 클라이언트 라우팅 또한 수행한다. 이러한 방식은 낮설게 느껴진다.
Next.js는 SSR을 비록한 사전 렌더링을 지원하기에 최초 페이지 렌더링이 서버에서 수행되는 것을 알고 있다.
index.tsx를 아래와 같이 수정해보자.
import Link from 'next/link';
const Home = () => {
return (
<ul>
<li>
{/* next의 eslint 룰을 끄기 위해 추가 */}
{/* eslint-disable-next-line */}
<a href="/hello">A 태그로 이동</a>
</li>
<li>
<Link prefetch={false} href="/hello">
Link 컴포넌트로 이동
</Link>
</li>
</ul>
);
}
export default Home;
next/link는 Next.js에서 제공하는 라우팅 컴포넌트인데 a 태그와 비슷한 동작을 한다.
비슷해 보이지만 a 태그는 블링크 현상 이후에 페이지 라우팅이 진행되지만, next/link를 활용한 이동은 SPA 처럼 블링크 현상이 없이 라우팅된다.
a 태그는 페이지를 만드는 데 필요한 모든 리소스를 전부 가져오고, 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의 Link 컴포넌트로 이동할 경우 클라이언트에서 필요한 js 파일만 불러온 뒤 라우팅한다는 것을 알 수 있다.
즉, 사용자가 바르게 볼 수 있는 최초 페이지를 제공한다는 점과 SPA와 같은 자연스러운 라우팅이라는 두 가지 장점을 살리고 있으며, 이를 잘 활용하려면 아래의 규칙을 준수하는 것이 좋다.
- <a> 대신 <Link>를 사용
- 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: {},
// }
// }
해당 함수가 있는 상태와 없는 상태를 빌드해서 살펴보자.
해당 함수가 없을 때는 SSR이 필요없는 정적 페이지로 분류되었다. 이는 getServerSideProps 함수가 없으면 SSR이 필요없으며 빌드 시점에 미리 만들어도 되는 페이지로 간주한다. 따라서 페이지에 접속해도 아까 봤던 "서버"라는 로그는 확인할 수 없다.
이로서 확인할 수 있는 점은 Next.js는 SSR 프레임워크지만 모든 작업이 서버에서만 일어나지 않다는 것이다.
api/hello.ts
서버의 api를 정의하는 폴더이다.
해당 주소는 다른 pages 파일과는 달리 HTML 요청을 하는 것이 아니라 단순히 서버 요청을 주고받게 된다.
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
type Data = {
name: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
res.status(200).json({ name: "John Doe" });
}
여기에 있는 코드는 오직 서버에서만 실행되며 일반적인 프론트 프로젝트를 만든다면 볼 일은 없다.
하지만 서버에서 내려주는 데이터를 조합해 BFF(Backend-for-Frontend) 형태로 사용하거나, 풀스택 애플리케이션을 구축하는 경우, CORS 문제를 우회하는 경우 사용할 수 있다.
Data Fetching
Next.js에서는 SSR 지원을 위한 몇 가지 데이터 불러오기 전략이 있는데, 이를 Data Fetching 이라 한다.
이 함수는 pages/의 폴더에 있는 라우팅이 되어 있는 파일에서만 사용할 수 있으며 정해진 함수명으로 export를 사용해 함수 파일을 외부로 보내야 한다.
서버에서 미리 필요한 페이지를 만들어 제공하거나 해당 페이지에 요청이 있을 때마다 서버에서 데이터를 조회해서 미리 페이지를 만들어 제공할 때 사용한다.
getStaticPaths와 getStaticProps
어떤 페이지를 CMS(Contents Management System)나 블로그, 게시판과 같이 사용자와 관계없이 정적으로 결정된 페이지를 보여주고자 할 때 사용되는 함수다. 이 두 함수는 무조건 두 함수가 동시에 존재해야 사용할 수 있다.
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 렌더링
}
true인 경우 빌드되기 전까지는 fallback 컴포넌트를 보여주고 빌드가 완료되면 해당 페이지를 보여준다.
만약 "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가 추가되었나?
리액트의 SSR의 작동을 생각해보자.
- 서버에서 fetch 등으로 렌더링에 필요한 정보를 가져온다.
- 가져온 정보를 바탕으로 HTML을 작성
- 2번 정보를 브라우저에 제공
- 3번 정보로 hydrate 작업 진행
- hydrate 작업 결과물과 서버의 HTML이 다르면 불일치 에러를 뱉음
- 5번 작업 역시 fetch 등을 활용해야 한다.
즉 1~6번 작업 사이의 fetch 시점에 따라 결과의 불일치가 일어날 수 있기에 1번 정보를 script 형태로 저장하는 것이다. 이로인해 1번 작업을 6번에서 반복하지 않아도 되기에 효율적이다.
즉, getServerSideProps의 props로 내려줄 수 있는 값은 JSON으로 직렬화 할 수 있는 값을 전달해야 한다.
JSON.stringify로 직렬화 할 수 있는 값만 제공해야 하고, 값에 대해 가공이 필요하다면 실제 페이지 컴포넌트에서 하는 것이 옳다.
추가적으로 해당 함수는 무조건 서버에서만 실행되기에 아래와 같은 제약이 있다.
- window.document와 같은 브라우저에서 접근할 수 있는 객체에는 접근❌
- API 호출시 완전한 주소를 제공
서버는 자신의 호스트를 유추할 수 없기 때문 - 만약 에러가 발생한다면 에러 페이지로 redirect됨
항상 getServerSideProps는 서버에서 실행되는 함수라는 것을 기억하자.
컴포넌트 내 DOM에 추가하는 이벤트 핸들러와 useEffect 등은 서버에서 실행되지 않도록 별도로 처리 해야한다.
따라서 getServerSideProps 내부에서 실행하는 내용은 최대한 간결하게 작성하기 위해 꼭 최초에 보여줘야 하는 데이터가 아니면 해당 함수보다 클라이언트에서 호출하는 것이 더 유리하다.
또한 조건에 따라 다른 페이지로 보내는 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에서 SSR을 더 잘 표현하기 위한 핵심 함수이니 잘 기억해두자.
getInitialProps
getStaticProps나 getServerSideProps가 나오기 전에 사용할 수 있었던 유일한 페이지 데이터를 불러오기 수단이었으며, 대부분의 경우에서 getStaticProps나 getServerSideProps를 사용하는 것을 권장한다.
하지만 과거에 작성된 Next.js 코드나 _app.tsx와 같은 일부 페이지에서는 getInitialProps만 사용할 수 있다....
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를 추가할 수 있다.
이 방식은 다른 컴포넌트의 클래스명과 겹쳐 발생하는 스타일 충돌이 일어나지 않도록 고유 클래스명을 제공한다.
// button.modules.css
.alert {
color : red;
}
.Button_alert__32TJN
SCSS & SASS
SCSS와 SASS 역시 CSS와 같은 방식으로 사용할 수 있다.
CSS-in-JS
해당 방식은 코드 작성의 편의성 이외의 성능이점 이나 여러 부분에서 논쟁거리 이지만 분명 JS 내부에서 CSS을 작성할 수 있어 직관적으로 편리할 수 있는 방식이다.
Guides: CSS-in-JS | Next.js
Use CSS-in-JS libraries with Next.js
nextjs.org
_app.tsx 응용하기
_app.tsx는 Next.js로 만든 모든 서비스가 진입하는 최초 진입점이란 것을 안다. 이러한 특징을 잘 이해하고 이용하면 사용자가 처음 접근했을 때 하고 싶은 무언가를 여기에서 처리할 수 있다.
우선 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
}
해당 코드가 포함된 애플리케이션의 실행 절차는 아래와 같다.
- 자체 페이지에 getInitialProps가 있는 곳을 방문
- getServerSideProps가 있는 페이지를 <Link>를 이용해 방문
- 1번 페이지를 <Link>로 방문
- 2번 페이지를 <Link>로 방문
Link를 활용하면 라우팅은 클라이언트 렌더링 처럼 작동한다. 페이지 방문 최초 시점인 1번은 SSR이 전체저긍로 작동해야 해서 페이지 전체를 요청한다. 그러나 이후에는 클라이언트 라우팅을 수행하기 위해 getServerSideProps 결과를 json 파일만 요청해서 가져온다.
이는 웹 서비스에 최초로 접근했을 때만 실행하고 싶은 내용을 app.getInitialProp 내부에 담아둘 수 있다는 것이다.
이는 userAgent나 사용자 정보와 같은 애플리케이션 전역에 걸쳐 사용해야 하는 정보 호출등의 작업을 넣어둔다면 편할 것이다.
next.config.js
해당 파일은 Next.js 실행에 필요한 설정을 추가할 수 있는 파일이다.
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig
아래에서 실무에서 사용되는 설정만 간단하게 살펴보자.
basePath : URL을 위한 기본 주소값
만약 해당 값을 docs로 둔다면 localhost:300/docs가 기본 URL이 된다.
powerdByHeader
Next.js는 응답 관련 헤더에 X-Power-by : Next.js 정보를 제공하는데 이 정보를 제거할 수 있다. 하지만 보안 관련 솔루션에서는 해당 부분을 취약점으로 분류하니 false로 해둡시다...
redirects
특정 주소를 다른 주소로 보내고 싶을 때 사용된다.
지금까지 간략하게 Next.js에 대해 알아봤는데, 앞서 팀 프로젝트나 과제 프로젝트에서 사용해본 Next.js라 이해하는데에 큰 어려움은 없었다. 하지만 SSR 프레임워크지만 SPA 처럼 자연스러운 라우팅을 지원하는 원리에 대해 알게되어 궁금증이 해소되어 재미있었다.
'React > 모던 리액트 Deep Dive' 카테고리의 다른 글
[React Deep Dive] 리액트 훅으로 시작하는 상태 관리 (0) | 2025.06.12 |
---|---|
[React Deep Dive] 리액트와 상태 관리 라이브러리 (1) | 2025.06.10 |
리액트와 서버사이드 (0) | 2025.06.03 |
서버 사이드 렌더링 (1) | 2025.04.30 |
사용자 정의 훅과 고차 컴포넌트 (0) | 2025.04.28 |