[Next.js] Next.js 13 ์์๋ณด๊ธฐ
React ํ๋ก์ ํธ๋ฅผ ํ๋ฉด, ์ญ์คํ๊ตฌ๋ Next.js๋ฅผ ์ฐ๊ฒ ๋๋ค. ์ต๊ทผ์ ๋ธ๋ก๊ทธ๋ฅผ ๊ฐํธํ๋ฉด์ create-next-app์ ๋๋ ธ๋๋ฐ, 13๋ฒ์ ์ผ๋ก ์
๋ฐ์ดํธ ๋๋ฉด์ ๋์ ๋๋ ๋ณ๊ฒฝ์ ์ด ์๋ ๋ฏ ํ๋ค.
๊ทธ ์ค ๋ช๋ช ๋ณ๊ฒฝ์ ์ ๊ธฐ์กด ์ฌ์ฉ๋ฒ๊ณผ ์์ดํด์ ธ์ ์ฐ๋๋ฐ ์ด์ง ์ ๋ฅผ ์ข ๋จน์๋๋ฐ, ์ด๋ฌํ ๋ณ๊ฒฝ์ ์ ๋ํด ๋ค๋ค๋ณด๊ณ ์ ํ๋ค.
13๋ฒ์ ์ด ๋๋ฉด์ ์ ์๋ ๋ณ๊ฒฝ์ ์ ์๋์ ๊ฐ๋ค.
appDirectory: ๋ ์ฝ๊ณ , ๋ ๋น ๋ฅด๊ณ , ๋ ์ ์ ํด๋ผ์ด์ธํธ JavaScript- Layouts
- React Server Components
- Streaming
- Turbopack: ์ต๋ 700๋ฐฐ ์ด์ ๋น ๋ฅธ Rust ๊ธฐ๋ฐ Webpack ์ ์ฉ
- New
next/image: ๋ค์ดํฐ๋ธ ๋ธ๋ผ์ฐ์ ์ lazy loading ๊ธฐ๋ฒ์ผ๋ก ๋์ฑ ๋นจ๋ผ์ง ์ด๋ฏธ์ง ์ปดํฌ๋ํธ - New
@next/font: Zero Layout Shift๋ฅผ ์ํ ์ ํ ํธ์คํ ํฐํธ - Improved
next/link: ์๋aํ๊ทธ๋ก ๋จ์ํ๋ ๋งํฌ API
Layout Shift๋, ํด๋ผ์ด์ธํธ์ ๋ ๋๋ง ๊ณผ์ ์์ ์ฌ์ฉ์์๊ฒ ๋ ๋๋ง ์ด์ ์ UI๊ฐ ๋ณด์ฌ์ง๋ ํ์์ ์๋ฏธํ๋ค. ๋น์ทํ ์ฉ์ด๋ก FOUC(Flash Of Unstyled Content)๊ฐ ์๋ค.
๋น๋๊ธฐ ๋ก๋ฉ ์์(ํฐํธ, ์ด๋ฏธ์ง ๋ฑ)์ ์ง์ฐ์ ์ํด ๋ฐ์ํ๋ฉฐ, ์ฌ์ฉ์์๊ฒ ์๋ํ์ง ์์ UI๋ฅผ ๋ณด์ฌ์ฃผ์ด ์ฌ์ดํธ์ ์ ๋ขฐ๋ ๋ฐ ๋ฏธ๊ด์ ํด์น ์ ์๋ค.
์ด ์ค ๋ช ๊ฐ์ง๋ ๋ด๋ถ์ ์ธ ์ฑ๋ฅ ํฅ์์ด๋ผ ์ฝ๋ ์ฌ์ฉ์์ ๋ณ๊ฒฝ์ ์ ์๋ค.
12๋ฒ์ ๋ถํฐ experimental ๊ธฐ๋ฅ์ผ๋ก ์ ๊ณต๋๋ app Directory ๊ธฐ๋ฅ์ด ๋ฉ์ธ์ผ๋ก ์ฌ๋ผ์จ ๊ฒ ๊ฐ๋ค. ๊ทธ ๋น์ ์ฌ์ฉํด๋ณผ๊น ํ๋ค๊ฐ ์คํ์ ๊ธฐ๋ฅ์ด๋ผ๊ธธ๋ ๋๊ฒผ๋ ๊ธฐ์ต์ด ์๋ค.
13๋ฒ์ ์์ ๊ฐ์ฅ ํฌ๊ฒ ์ฒด๊ฐ๋๋ ๋ณ๊ฒฝ์ ์ด๋ค. ์ด ํญ๋ชฉ์ ์ฃผ์ ๊ธฐ๋ฅ ์ค ํ๋์ธ ๋ผ์ฐํ ์ ๋ํ ๋ณ๊ฒฝ์ ์ด๋ค.
์๋์ ์์๋ TypeScript๋ฅผ ๊ธฐ์ค์ผ๋ก ํ๋ค.
๊ธฐ์กด
๊ธฐ์กด ํ๋ก์ ํธ ๊ตฌ์กฐ๋ ์๋์ ๊ฐ๋ค.
BASH1 2 3 4 5 6 7๐ฆsrc โฃ ๐page โ โฃ ๐mypage โ โ โฃ ๐info.tsx # /mypage/info โ โ โ ๐update.tsx # /mypage/update โ โฃ ๐index.tsx # / โ โ ๐login.tsx # /login
์์ ๊ฐ์ด page๋ผ๋ ํด๋์ ํ์ด์ง ํ์ผ์ ์์ฑํ๋ ํํ๋ค. ํ์ผ์ ์ด๋ฆ์ด ๊ณง URL์ด ๋๋ค. page/ ํ์์ ๋ฐ๋์ ํ์ด์ง ์ปดํฌ๋ํธ๋ง ์์ด์ผํ๋ฏ๋ก, ์๋ธ ์ปดํฌ๋ํธ๋ ๋์์ธ๊ฐ์ ํ์ผ์ด ์์นํด์ ์ ๋๋ค๋ ๋จ์ ์ด ์๋ค.
๋ณ๊ฒฝ
Next.js 13์ app Directory๋ฅผ ์ ์ฉํ๋ฉด ์๋์ ๊ฐ์ ๊ตฌ์กฐ๊ฐ ๊ธฐ๋ณธ์ด ๋๋ค.
BASH1 2 3 4 5 6 7 8 9 10๐ฆsrc โฃ ๐app โ โฃ ๐login โ โ โ ๐page.tsx # /login โ โฃ ๐mypage โ โ โฃ ๐info โ โ โ โ ๐page.tsx # /mypage/info โ โ โ ๐update โ โ โ ๐page.tsx # /mypage/update โ โ ๐page.tsx # /
ํ์ผ๋ช
์ด URL์ด์๋ ๊ธฐ์กด๊ณผ ๋ฌ๋ฆฌ ํด๋๋ช
์ด URL์ด ๋๋ฉฐ, ํด๋๋ช
ํ์์๋ ๋ฐ๋์ page ํ์ผ์ด ์์ด์ผํ๋ค.
๋ณ๊ฒฝ๋ ๋ผ์ฐํ ์์ , ํ์ผ๋ช ๋ง๋ค ๋์ํ๋ ์ญํ ์ด ์ ํด์ ธ ์๋๋ฐ, ๊ฐ๊ฐ์ ์ญํ ์ ์๋์ ๊ฐ๋ค.
| ํ์ผ๋ช | ์ ์ |
|---|---|
layout | ํ์ฌ ๋ฐ ํ์ ํ์ด์ง์ ๊ณต์ UI ์ปดํฌ๋ํธ |
page | ํ์ด์ง ์ปดํฌ๋ํธ |
loading | ํ์ฌ ๋ฐ ํ์ ํ์ด์ง์ ๋ก๋ฉ UI ์ปดํฌ๋ํธ |
not-found | ํ์ฌ ๋ฐ ํ์ ๊ฒฝ๋ก์ 404 ์ค๋ฅ UI ์ปดํฌ๋ํธ |
error | ํ์ฌ ๋ฐ ํ์ ๊ฒฝ๋ก์ ์ค๋ฅ UI ์ปดํฌ๋ํธ |
global-error | ์ ์ญ ์ค๋ฅ UI ์ปดํฌ๋ํธ |
route | ์๋ฒ์ฌ์ด๋ API ์๋ํฌ์ธํธ |
template | ํ์ด์ง ํ ํ๋ฆฟ UI ์ปดํฌ๋ |
default | ๋ณ๋ ฌ ๋ผ์ฐํธ์ ๋์ฒด UI ์ปดํฌ๋ํธ |
๊ฐ ํด๋ ํ์์ ์์ ๊ฐ์ด ์ ์๋ ํ์ผ๋ช ์ ์ถ๊ฐํ๋ฉด, ๋ณ๋์ ์ฐ๊ฒฐ์์ ์์ด ํ์ผ์ ์ ์ธํ๋๊ฒ ๋ง์ผ๋ก๋ ์์ ๊ฐ์ ๋์์ ์ํํ ์ ์๋ค.
Next.js 13์ ํ์ด์ง ๋ ๋๋ง ๊ตฌ์กฐ
Next.js์ ๊ณต์๋ฌธ์์์ ๊ฐ ์ปดํฌ๋ํธ์ ๋์์ ์์ ๊ฐ์ด ๋ฌ์ฌํ๋ค. Layout ์ปดํฌ๋ํธ์ Template ์ปดํฌ๋ํธ๊ฐ ๊ฐ์ธ์ง๊ณ , ์ ์ธ๋ ๋ก๋ฉ, ์๋ฌ ์ปดํฌ๋ํธ๋ค์ด ๋ฌถ์ธ ๋ค์ ๋น๋ก์ ์ค์ ํ์ด์ง ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋๋ค.
ํนํ ์ด ์ค์์ app ๋ฐ๋ก ํ์์ ์์นํ๋ layout ์ปดํฌ๋ํธ๋ ๋ชจ๋ ํ์ด์ง์ ๊ณตํต์ผ๋ก ์ ์ฉ๋๋ ์ ์ญ ๋ ์ด์์์ด๋ค. ์ด์ ๋ฒ์ ์ _app, _document์ ์ญํ ์ ๋์ฒดํ๋ค.
layout, loading ๋ฑ, ์ ํ์์ ํ์ฌ ๋ฐ ํ์๋ผ๊ณ ๋ช
์๋ ์ปดํฌ๋ํธ๋ค์ ์ ๋ถ ํ์ ๊ฒฝ๋ก์๋ ์ ์ฉ๋๋ ์ปดํฌ๋ํธ๋ค์ด๋ค.
Next.js 13์ ํ์ ํ์ด์ง ๋ ๋๋ง ๊ตฌ์กฐ
ํ์ ์ปดํฌ๋ํธ๋ ์์ ๊ฐ์ด ๋ ๋๋ง๋๋ค. ๋ถ๋ชจ์ ๊ณต์ ์ปดํฌ๋ํธ๋ค์ด ๊ฐ์ด ๋ ๋๋ง๋๋ ๊ฒ์ ํ์ธํ ์ ์๋ค. ๋ฎ์ด์์ฐ๋ ๊ฒ ์๋๋ ์ฃผ์ํ์.
์ด๋ฅผ ์ฝ๋๋ก ๋ณด๋ฉด ์๋์ ๊ฐ๋ค.
app/layout.tsx
TSX1 2 3 4 5 6 7 8 9 10// app/layout.tsx export default function Layout({children}: PropsWithChildren) { return ( <div style={{backgroundColor: '#8F85ED', padding: 20}}> <p>app/layout</p> {children} </div> ) }
app/template.tsx
TSX1 2 3 4 5 6 7 8 9 10// app/template.tsx export default function Template({children}: PropsWithChildren) { return ( <div style={{backgroundColor: '#8B9DF7', padding: 20}}> <p>app/template</p> {children} </div> ) }
app/page.tsx
TSX1 2 3 4 5 6 7 8// app/page.tsx export default function Page() { return ( <div style={{backgroundColor: '#8AAFE1', padding: 20}}> <p>app/page.tsx</p> </div> ) }
๊ฒฐ๊ณผ๋ ์๋์ ๊ฐ์ด ๋ ๋๋ง๋๋ค. (CSS๋ ์์ ์ ์ฉ)
๋ ๋๋ง ๊ฒฐ๊ณผ
์์ ๊ทธ๋ฆผ๊ณผ ๊ฐ์ด, <Layout /> ์ปดํฌ๋ํธ๊ฐ ๊ฐ์ฅ ๋ฐ๊นฅ์ ์์นํ๊ณ , ๊ทธ ์์์ผ๋ก ๋์ผ ๊ฒฝ๋ก์ <Template /> ์ปดํฌ๋ํธ๊ฐ ์์นํ๋ค. ์ดํ ์ค์ <Page /> ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋์ด ํ๋์ ํ์ด์ง๊ฐ ์์ฑ๋๋ค.
๋ง์ฝ, app/ ํ์์ main์ด๋ผ๋ ํด๋๋ฅผ ๋์ด ํ์ ๊ฒฝ๋ก๋ฅผ ์์ฑํ๋ฉด ์ด๋ป๊ฒ ํํ๋ ๊น?
main/ ํด๋์ ์๋์ ๊ฐ์ ํ์ผ์ด ์๋ค๊ณ ๊ฐ์ ํ์.
app/home/layout.tsx
TSX1 2 3 4 5 6 7 8 9 10// app/home/layout.tsx export default function Layout({children}: PropsWithChildren) { return ( <div style={{backgroundColor: '#ED8774', padding: 20}}> <p>app/home/layout</p> {children} </div> ) }
app/home/template.tsx
TSX1 2 3 4 5 6 7 8 9 10// app/home/template.tsx export default function Template({children}: PropsWithChildren) { return ( <div style={{backgroundColor: '#F7A079', padding: 20}}> <p>app/home/template</p> {children} </div> ) }
app/home/page.tsx
TSX1 2 3 4 5 6 7 8// app/home/page.tsx export default function Page() { return ( <div style={{backgroundColor: '#E1A97A', padding: 20}}> <p>app/home/page.tsx</p> </div> ) }
๋ ๋๋ง ๊ฒฐ๊ณผ
๋ถ๋ชจ์ ๊ณต์ ์ปดํฌ๋ํธ <Layout />, <Template />๊ฐ ์์์ ๋ ๋๋ง๋๊ณ , ๊ทธ ํ์์ main/ ํด๋์ ์์๋ค์ด ๋ ๋๋ง๋๋ ๊ฑธ ํ์ธํ ์ ์๋ค.
์ด ๊ตฌ์กฐ๋ฅผ ์ ํ์ฉํ๋ฉด ์๋น์ค ์ ์ญ์ ์ ์ฉํด์ผํ ๋ ์ด์์์ ์ฝ๊ฒ ์ ์ฉํ ์ ์์ผ๋ฉฐ, ๊ฐ ํ์ด์ง ์ฃผ์ ๋ณ๋ก URL์ ๊ทธ๋ฃนํํด์ ๋ ์ด์์์ ์ง๊ธฐ์๋ ์ฉ์ดํ๋ค. ๋ํ, ์ด๋ฐ์์ผ๋ก ๊ณต์ ์ปดํฌ๋ํธ๋ฅผ ์๋ง๊ฒ ํ์ฉํ๋ฉด, ๋ถํ์ํ ์ปดํฌ๋ํธ์ ๋ ๋๋ง์ ๋ฐฉ์งํ ์ ์๋ค.
๋ค๋ง ์ด๋ฐ ๊ตฌ์กฐ ์ ์ฃผ์ํ ์ ์ด ํ๋ ์๋ค๋ฉด, ์์์ ์ ์ธ๋ ๊ณต์ ์ปดํฌ๋ํธ๋ฅผ ํ์์์ ์์๋ก ์ ์ธํ ์ ์๋ ๊ฒ ๊ฐ๋ค. ์ฆ, ํน์ ํ์ ๊ฒฝ๋ก์์ ๋ถ๋ชจ์ layout.tsx ํน์ template.tsx๋ฅผ ์ ์ธ์ํฌ ์ ์์ผ๋, ๊ณต์ ์ปดํฌ๋ํธ๋ฅผ ์ค๊ณํ ๋, ์ด๋ฌํ ์ผ์ด์ค๊ฐ ๋ฐ์ํ์ง ์๊ฑฐ๋, ๋ฐ์ํด๋ ์ํฅ์ ๋ฏธ์น์ง ์๋๋ก ์ ์ค๊ณํด์ผํ ๊ฒ ๊ฐ๋ค.
์ด ๋ฐ์ ๋ ์์ธํ ๋ด์ฉ์ ๊ณต์๋ฌธ์์์ ํ์ธํ์.
Next.js์ ์ปดํฌ๋ํธ๋ ์๋ฒ์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ก ๊ตฌ๋ถ๋๋ค. ๊ฐ ์ปดํฌ๋ํธ์ ๊ตฌ๋ถ์ ๋ฐ๋ผ ์ํ ๊ฐ๋ฅํ ์ญํ ์ด ์กฐ๊ธ์ฉ ๋ค๋ฅด๋ค.
| ๊ธฐ๋ฅ | server | client |
|---|---|---|
| ๋ฐ์ดํฐ Fetch | โ | โ |
| ๋ฐฑ์๋ ๋ฆฌ์์ค์ ์ง์ ์ ๊ทผ | โ | โ |
| ์๋ฒ์ ๋ฏผ๊ฐํ ์ ๋ณด ๊ด๋ฆฌ (ํ ํฐ ๋ฑ) | โ | โ |
| ์๋ฒ์ ํฐ ์ข ์์ฑ ์ ์ง / ํด๋ผ์ด์ธํธ ์ธก JS ์ฝ๋ ์ถ์ | โ | โ |
| ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ฌ์ฉ (onChange ๋ฑ) | โ | โ |
| ์ํ๊ด๋ฆฌ ๋ฐ ์๋ช ์ฃผ๊ธฐ ์ฌ์ฉ (useEffect ๋ฑ) | โ | โ |
| ๋ธ๋ผ์ฐ์ ์ฉ API ์ฌ์ฉ (Navigator API ๋ฑ) | โ | โ |
| ์ปค์คํ Hook ์ฌ์ฉ | โ | โ |
| React class ์ปดํฌ๋ํธ ์ฌ์ฉ | โ | โ |
๊ฒฐ๊ตญ ํด๋น ์ปดํฌ๋ํธ์ ๋ ๋๋ง ๋ฐฉ์์ด SSR์ด๋, CSR์ด๋ ๋ช
์ํ๋ ๊ฒ์ด๋ค. ์ด์ ๋ฒ์ ์ ๊ฒฝ์ฐ, ๋ณ๋์ ์ ์ธ ์์ด, getServerSideProps, getStaticProps ๊ฐ์ ๋ฉ์๋์ ์ฌ์ฉ์ ๋ฐ๋ผ ๊ตฌ๋ถํ์๋ค.
13 ๋ฒ์ ๋ถํฐ๋ ์ปดํฌ๋ํธ ํ์ผ ์๋จ์ use client๋ฅผ ์์ฑํ์ฌ ๊ตฌ๋ถํ๋ค. ๊ธฐ๋ณธ์ ์๋ฒ ์ปดํฌ๋ํธ๋ก, ์๋ฌด๊ฒ๋ ๋ช
์ํ์ง ์์ผ๋ฉด ์๋ฒ ์ปดํฌ๋ํธ๋ก ๋์ํ๋ค.
์ด๋ฅผ ์ฝ๋๋ก ๋ณด๋ฉด ์๋์ ๊ฐ๋ค.
TSX1 2 3 4 5 6// ์๋ฒ ์ปดํฌ๋ํธ (๋ช ์ํ์ง ์์๋ ๋จ) export default function Component(): ReactNode { // ... }
TSX1 2 3 4 5 6 7// ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ 'use client' export default function Component(): ReactNode { // ... }
Next.js ์ค์น ์ ๊ธฐ๋ณธ์ ์ผ๋ก ํฌํจ๋๋ ESLint์ eslint-config-next ์ค์ ํ๋ฌ๊ทธ์ธ์ ํ์ฉํ๋ฉด, ๊ฐ ์ปดํฌ๋ํธ๋ง๋ค ๋ถ๊ฐ๋ฅํ ๋ก์ง์ ์ก์์ค๋ค. ์ด๋ฅผํ
๋ฉด ์๋ฒ ์ปดํฌ๋ํธ์์ useState, ๋ธ๋ผ์ฐ์ ์ด๋ฒคํธ์ ์ฌ์ฉ์ ๊ฐ์งํ๊ณ ๊ฒฝ๊ณ ํ๋ ์.
์ปดํฌ๋ํธ์ ๋ํ ์์ธํ ์ ๋ณด๋ ๊ณต์๋ฌธ์์์ ํ์ธํ์.
์ ๋ ๋ณ๊ฒฝ์ ๊ณผ ๋ฌ๋ฆฌ, ์ด๊ฑด ์ถ๊ฐ๋ ๊ธฐ๋ฅ๊ฐ์๋ฐ, ํฐํธ ์ ์ฉ์ ํธ์์ฑ์ ๋์ธ ๊ธฐ๋ฅ์ด๋ค. next/font๋ฅผ ํ์ฉํ๋ฉด, ๊ธฐ์กด์ ๋ฐฉ์์ ๋นํด ํฐํธ๋ฅผ ์์ฝ๊ฒ ์ ์ฉํ ์ ์๋ค.
์ง์ํ๋ ๊ณต์ ์๋น์ค๋ ์์ง ํ๋๋ค. ๊ทธ ๋ฐ์ ๋ก์ปฌ ํฐํธํ์ผ์ ์ฐ๊ฒฐ๋ ์ง์ํ๋ค.
Google Font๋ ์๋์ ๊ฐ์ด ์ ์ธํด์ ์ฌ์ฉํ ์ ์๋ค.
TSX1 2 3 4 5 6 7 8 9 10 11 12 13import { Noto_Sans_KRr } from 'next/font/google'; export const notoSans = Noto_Sans_KR({ subsets: [ 'latin' ], weight: [ '100', '300', '400', '500', '700', '900' ] }); export default function Component(): ReactNode { return ( <div> <div className={notoSans.className}>className ์ ์ฉ</div> <div style={{ fontFamily: notoSans.style.fontFamily }}>font-family ์ ์ฉ</div> </div> ); }
์ ์ฝ๋๋ ์ ๋ช ํ ํ๊ธ ๊ธ๊ผด ์ค ํ๋์ธ ์ ์ ์ฉํ ์์๋ค.
className์ผ๋ก ์ ์ฉํ๋ ๋ฐฉ์๊ณผ font-family๋ก ์ ์ฉํ๋ ๋ฐฉ์ ๋ ๊ฐ์ง๊ฐ ์๋ค.
ํฐํธ ํ์ผ์ ๋ก์ปฌ ํน์ CDN์ผ๋ก ๋ฐ์ CSS๋ฅผ ์ ์ฉํ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ๊ฐ๋จํ์ฌ, Google Font์์ ์ํ๋ ํฐํธ๋ฅผ ๊ณ ๋ฅด๊ณ ์ด๋ฅผ next/font๋ก ์ฐพ์ ์ ์ฉํ๋ฉด ๋๋ค.
next/font์ ๊ฐ์ฅ ํฐ ๋จ์ ์ Google Font ์ด์ธ์ ์ง์ํ๋ ์๋น์ค๊ฐ ์์ง ์๋ค๋ ๊ฒ์ด๋ค. ๋คํํ, ๋ก์ปฌ ํฐํธ๋ ์ง์ํด์ฃผ๊ธฐ ๋๋ฌธ์, Google Font์ ๋ฑ๋ก๋์ง ์์ ํฐํธ๋ ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
TSX1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31import localFont from 'next/font/local'; const pretendard = localFont({ src: [ { path: './pretendard-regular.otf', weight: 'normal', style: 'normal', }, { path: './pretendard-bold.otf', weight: 'bold', style: 'normal', }, { path: './pretendard-italic.otf', weight: 'normal', style: 'italic', }, ], }); export default function Component(): ReactNode { return ( <div> <div className={pretendard.className}>className ์ ์ฉ</div> <div style={{ fontFamily: pretendard.style.fontFamily }}>font-family ์ ์ฉ</div> </div> ); }
์์ ๊ฐ์ด ํฐํธ์ ๊ฒฝ๋ก์ ์ง์ ์ ๊ทผํ์ฌ ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
ํฐํธ์ ๋ํ ๋์ฑ ์์ธํ ์ ๋ณด๋ ๊ณต์๋ฌธ์์์ ํ์ธํ์.
![[recoil] Duplicate atom key ์ค๋ฅ ํด๊ฒฐํ๊ธฐ](https://user-images.githubusercontent.com/50317129/216662084-69f29d33-1956-42a1-90d6-80d311949d10.png)
![[TypeScript] ๋งํฌ๋ค์ด TOC ๋ง๋ค๊ธฐ](https://user-images.githubusercontent.com/50317129/270418234-c6951309-6bad-4e82-82a8-244585f54735.jpg)