์˜ค๋Š˜์€ ๋ธŒ๋ผ์šฐ์ € ๋ Œ๋”๋ง ๋ฐฉ์‹์ธ SSG, CSR, SSR, ISR ๋‚ด๋ถ€ ์ž‘๋™์›๋ฆฌ์— ๋Œ€ํ•ด ์‚ดํŽด๋ณผ ๊ฒƒ์ด๋‹ค.

 

1. SSG (Static Site Generation) ์ž‘๋™์›๋ฆฌ

์ „์ฒด ํ๋ฆ„๋„

[๋นŒ๋“œ ํƒ€์ž„]                    [๋Ÿฐํƒ€์ž„]
๊ฐœ๋ฐœ์ž ์ฝ”๋“œ                     ์‚ฌ์šฉ์ž ์š”์ฒญ
    ↓                              ↓
npm run build                  CDN/Server
    ↓                              ↓
๋ฐ์ดํ„ฐ ํŽ˜์นญ (API ํ˜ธ์ถœ)         ์ •์  HTML ํŒŒ์ผ ์ „์†ก
    ↓                              ↓
HTML ์ƒ์„ฑ (๋ชจ๋“  ํŽ˜์ด์ง€)        ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ฆ‰์‹œ ๋ Œ๋”๋ง
    ↓                              ↓
.next/static ํด๋”์— ์ €์žฅ       (Hydration์œผ๋กœ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ)

 

1. ๋นŒ๋“œํƒ€์ž„(Build Time)

  • ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  nun run build ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด, ๋ชจ๋“  ํŽ˜์ด์ง€๊ฐ€ ๋ฏธ๋ฆฌ HTML๋กœ ์ƒ์„ฑ๋œ๋‹ค.
  • ์ด ๊ณผ์ •์—์„œ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋‹ค๋ฉด API๋ฅผ ํ˜ธ์ถœํ•ด ๊ฐ€์ ธ์˜ค๊ณ , ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ HTML ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด .next/static (Next๋ผ๋ฉด) ๊ฐ™์€ ํด๋”์— ์ €์žฅํ•œ๋‹ค.
  • ์ฆ‰, ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๊ธฐ ์ „ ๋ฏธ๋ฆฌ ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ์ค€๋น„ํ•˜๋Š” ๋‹จ๊ณ„์ด๋‹ค.

2. ๋Ÿฐํƒ€์ž„(Runtime)

  • ์‚ฌ์šฉ์ž๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๋ฉด, ์„œ๋ฒ„๋‚˜ CDN์ด ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋‘” ์ •์  HTMLํŒŒ์ผ์„ ์ฆ‰์‹œ ์ „์†กํ•œ๋‹ค.
  • ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ๋ฐ›์€ HTML์„ ๋ฐ”๋กœ ํ™”๋ฉด์— ๋ณด์—ฌ์ฃผ๊ณ , ํ•„์š”ํ•˜๋ฉด React์˜ Hydration ๊ณผ์ •์„ ํ†ตํ•ด ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๊ธฐ๋Šฅ(๋ฒ„ํŠผ ํด๋ฆญ, ํผ ์ž…๋ ฅ ๋“ฑ)์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ƒ์„ธ ์ž‘๋™ ๊ณผ์ •

1๋‹จ๊ณ„: ๋นŒ๋“œ ํƒ€์ž„ (Build Time)

npm run build

 

๋‚ด๋ถ€์—์„œ ์ผ์–ด๋‚˜๋Š” ์ผ

1. Next.js๊ฐ€ ๋ชจ๋“  ํŽ˜์ด์ง€ ์Šค์บ”
   โ”œโ”€ app/page.tsx
   โ”œโ”€ app/about/page.tsx
   โ”œโ”€ app/blog/[slug]/page.tsx
   โ””โ”€ ...

2. ๊ฐ ํŽ˜์ด์ง€๋ณ„๋กœ ์‹คํ–‰:
   โ”œโ”€ generateStaticParams() ํ˜ธ์ถœ (๋™์  ๋ผ์šฐํŠธ)
   โ”œโ”€ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ํ•จ์ˆ˜ ์‹คํ–‰
   โ”œโ”€ React ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง
   โ””โ”€ HTML ํŒŒ์ผ ์ƒ์„ฑ

3. ์ถœ๋ ฅ๋ฌผ:
   โ”œโ”€ .next/server/app/page.html
   โ”œโ”€ .next/server/app/about.html
   โ”œโ”€ .next/server/app/blog/post-1.html
   โ””โ”€ JavaScript ๋ฒˆ๋“ค (.next/static/chunks/)

 

Next.js SSG ๋‚ด๋ถ€์—์„œ ์ผ์–ด๋‚˜๋Š” ๊ณผ์ •

  1. ํŽ˜์ด์ง€ ์Šค์บ”
    • Next.js๋Š” ๋นŒ๋“œ ๊ณผ์ •์—์„œ ํ”„๋กœ์ ํŠธ ๋‚ด ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
    • ์˜ˆ๋ฅผ ๋“ค์–ด:
      • app/page.tsx → ํ™ˆ ํŽ˜์ด์ง€
      • app/about/page.tsx → ์†Œ๊ฐœ ํŽ˜์ด์ง€
      • app/blog/[slug]/page.tsx → ๋™์  ๋ธ”๋กœ๊ทธ ๊ธ€ ํŽ˜์ด์ง€
    • ์ด๋ ‡๊ฒŒ ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ์ž๋™์œผ๋กœ ์ฐพ์•„์„œ ์ฒ˜๋ฆฌ ๋Œ€์ƒ์œผ๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
  2. ํŽ˜์ด์ง€๋ณ„ ์ฒ˜๋ฆฌ
    • ๋™์  ๋ผ์šฐํŠธ ์ฒ˜๋ฆฌ: generateStaticParams() ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜์–ด, ๋™์  URL(slug ๋“ฑ)์— ํ•„์š”ํ•œ ๋ชจ๋“  ์กฐํ•ฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    • ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ: ๊ฐ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ํ•จ์ˆ˜(API ํ˜ธ์ถœ ๋“ฑ)๋ฅผ ์‹คํ–‰ํ•ด์„œ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค.
    • React ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง: ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ด์šฉํ•ด React ์ปดํฌ๋„ŒํŠธ๋ฅผ HTML๋กœ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
    • HTML ํŒŒ์ผ ์ƒ์„ฑ: ๋ Œ๋”๋ง๋œ ๊ฒฐ๊ณผ๋ฅผ ์ •์  HTML ํŒŒ์ผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  3. ์ถœ๋ ฅ๋ฌผ ์ƒ์„ฑ
    • ์ƒ์„ฑ๋œ HTML ํŒŒ์ผ๊ณผ JavaScript ๋ฒˆ๋“ค์ด ํ”„๋กœ์ ํŠธ ๋‚ด๋ถ€ .next ํด๋”์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
    • ์˜ˆ์‹œ:
      • .next/server/app/page.html → ํ™ˆ ํŽ˜์ด์ง€
      • .next/server/app/about.html → ์†Œ๊ฐœ ํŽ˜์ด์ง€
      • .next/server/app/blog/post-1.html → ๋ธ”๋กœ๊ทธ ๊ธ€
      • .next/static/chunks/ → React ๊ธฐ๋Šฅ๊ณผ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ๋ฅผ ์œ„ํ•œ JS ๋ฒˆ๋“ค

 

Next.js ์ฝ”๋“œ ์˜ˆ์ œ

// app/blog/[slug]/page.tsx

// 1. ๋นŒ๋“œ ์‹œ ์ƒ์„ฑํ•  ๊ฒฝ๋กœ ์ •์˜
export async function generateStaticParams() {
  console.log('๐Ÿ—๏ธ  ๋นŒ๋“œ ํƒ€์ž„: ๊ฒฝ๋กœ ์ƒ์„ฑ ์ค‘...');

  // API์—์„œ ๋ชจ๋“  ํฌ์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
  const posts = await fetch('<https://api.example.com/posts>')
    .then(res => res.json());

  // ๊ฐ ํฌ์ŠคํŠธ๋งˆ๋‹ค ์ •์  ํŽ˜์ด์ง€ ์ƒ์„ฑ
  return posts.map((post: any) => ({
    slug: post.slug, // /blog/post-1, /blog/post-2 ๋“ฑ
  }));
}

// 2. ๊ฐ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ ํŽ˜์นญ
export default async function BlogPost({
  params
}: {
  params: Promise<{ slug: string }>
}) {
  console.log(`๐Ÿ—๏ธ  ๋นŒ๋“œ ํƒ€์ž„: ${params.slug} ํŽ˜์ด์ง€ ์ƒ์„ฑ ์ค‘...`);
  const { slug } = await params;

  // ์ด fetch๋Š” ๋นŒ๋“œ ์‹œ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋จ
  const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
    cache: 'force-cache' // SSG์˜ ํ•ต์‹ฌ ์„ค์ •(DEFAULT)
  }).then(res => res.json());

  // React ์ปดํฌ๋„ŒํŠธ๋ฅผ HTML๋กœ ๋ณ€ํ™˜
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );

2๋‹จ๊ณ„: ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ

.next/
โ”œโ”€โ”€ static/
โ”‚   โ”œโ”€โ”€ chunks/          # JavaScript ์ฝ”๋“œ ์กฐ๊ฐ๋“ค
โ”‚   โ”‚   โ”œโ”€โ”€ main.js      # React ๋Ÿฐํƒ€์ž„
โ”‚   โ”‚   โ””โ”€โ”€ pages/       # ํŽ˜์ด์ง€๋ณ„ JS
โ”‚   โ””โ”€โ”€ css/             # CSS ํŒŒ์ผ๋“ค
โ”œโ”€โ”€ server/
โ”‚   โ””โ”€โ”€ app/
โ”‚       โ”œโ”€โ”€ page.html           # ๋ฏธ๋ฆฌ ์ƒ์„ฑ๋œ HTML
โ”‚       โ”œโ”€โ”€ about.html
โ”‚       โ””โ”€โ”€ blog/
โ”‚           โ”œโ”€โ”€ post-1.html     # ๊ฐ ํฌ์ŠคํŠธ์˜ HTML
โ”‚           โ””โ”€โ”€ post-2.html

 

์ƒ์„ฑ๋œ HTML ์˜ˆ์‹œ

<!DOCTYPE html>
<html>
<head>
  <title>My Blog Post</title>
  <link rel="stylesheet" href="/_next/static/css/app.css" />
</head>
<body>
  <!-- ์ด๋ฏธ ๋ Œ๋”๋ง๋œ ์ฝ˜ํ…์ธ  -->
  <article>
    <h1>๋‚ด ์ฒซ ๋ฒˆ์งธ ํฌ์ŠคํŠธ</h1>
    <div>
      <p>์ด๊ฒƒ์€ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค...</p>
    </div>
  </article>

  <!-- JavaScript๋Š” Hydration์„ ์œ„ํ•ด ๋กœ๋“œ -->
  <script src="/_next/static/chunks/main.js"></script>
  <script src="/_next/static/chunks/pages/blog/post-1.js"></script>
</body>
</html>

 

3๋‹จ๊ณ„: ์‚ฌ์šฉ์ž ์š”์ฒญ (Request Time)

์‚ฌ์šฉ์ž๊ฐ€ /blog/post-1 ์ ‘์†
         ↓
CDN/Server๊ฐ€ ๋ฏธ๋ฆฌ ์ƒ์„ฑ๋œ HTML ์ „์†ก
         ↓
๋ธŒ๋ผ์šฐ์ €๊ฐ€ HTML ์ฆ‰์‹œ ๋ Œ๋”๋ง (0.1์ดˆ)
         ↓
JavaScript ๋‹ค์šด๋กœ๋“œ & ์‹คํ–‰
         ↓
Hydration (HTML์— ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์—ฐ๊ฒฐ)
         ↓
์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•œ ํŽ˜์ด์ง€ ์™„์„ฑ

 


Hydration ๊ณผ์ •

// 1. ์„œ๋ฒ„์—์„œ ์ƒ์„ฑ๋œ HTML (์ •์ )
<button>ํด๋ฆญํ•˜์„ธ์š”</button>

// 2. JavaScript ๋กœ๋“œ ํ›„ Hydration
<button onClick={() => alert('ํด๋ฆญ!')}>ํด๋ฆญํ•˜์„ธ์š”</button>
//      ↑ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์ถ”๊ฐ€๋จ

 

Hydration ์ฝ”๋“œ ํ๋ฆ„:

// React๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ์ˆ˜ํ–‰
ReactDOM.hydrateRoot(
  document.getElementById('root'),
  <App />
);

// ๊ณผ์ •:
// 1. ๊ธฐ์กด HTML ์š”์†Œ ์ฐพ๊ธฐ
// 2. Virtual DOM ์ƒ์„ฑ
// 3. ๊ธฐ์กด DOM๊ณผ ๋น„๊ต
// 4. ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์—ฐ๊ฒฐ
// 5. ์ƒํƒœ ๊ด€๋ฆฌ ํ™œ์„ฑํ™”

 

 

2. ISR (Incremental Static Regeneration) ์ž‘๋™์›๋ฆฌ

์ „์ฒด ํ๋ฆ„๋„

[๋นŒ๋“œ ํƒ€์ž„]              [์ฒซ ์š”์ฒญ]            [revalidate ํ›„]
๋ชจ๋“  ํŽ˜์ด์ง€ ์ƒ์„ฑ      → ์ •์  HTML ์ œ๊ณต →         ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์žฌ์ƒ์„ฑ
                          ↓                    ↓
                    ์บ์‹œ๋œ ๋ฒ„์ „ ์ œ๊ณต           ์ƒˆ ๋ฒ„์ „์œผ๋กœ ๊ต์ฒด

 

 

ISR ์ƒ์„ธ ์ž‘๋™ ๊ณผ์ •

์‹œ๋‚˜๋ฆฌ์˜ค: revalidate: 60 ์„ค์ •

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
    next: {
      revalidate: 60 // 60์ดˆ๋งˆ๋‹ค ์žฌ๊ฒ€์ฆ
    }
  }).then(res => res.json());

  return <article>{post.title}</article>;
}

ํƒ€์ž„๋ผ์ธ ๋ถ„์„

์‹œ๊ฐ„         ์š”์ฒญ          ์‘๋‹ต            ๋‚ด๋ถ€ ๋™์ž‘
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
00:00    ๋นŒ๋“œ ์™„๋ฃŒ      -              HTML v1 ์ƒ์„ฑ
         (์ดˆ๊ธฐ ์ƒํƒœ)

01:00    ์‚ฌ์šฉ์ž A      HTML v1 ์ œ๊ณต    โœ… ์บ์‹œ ํžˆํŠธ
         ๋ฐฉ๋ฌธ          (0.1์ดˆ)         (ํƒ€์ž„์Šคํƒฌํ”„: 00:00)

01:30    ์‚ฌ์šฉ์ž B      HTML v1 ์ œ๊ณต    โœ… ์บ์‹œ ํžˆํŠธ
         ๋ฐฉ๋ฌธ          (0.1์ดˆ)         (์•„์ง 60์ดˆ ์•ˆ๋จ)

02:00    ์‚ฌ์šฉ์ž C      HTML v1 ์ œ๊ณต    ๐Ÿ”„ ์žฌ์ƒ์„ฑ ํŠธ๋ฆฌ๊ฑฐ!
         ๋ฐฉ๋ฌธ          (0.1์ดˆ)         (60์ดˆ ๊ฒฝ๊ณผ)
                                      ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ:
                                      - API ์žฌํ˜ธ์ถœ
                                      - HTML v2 ์ƒ์„ฑ
                                      - ์บ์‹œ ๊ต์ฒด (10์ดˆ ์†Œ์š”)

02:05    ์‚ฌ์šฉ์ž D      HTML v1 ์ œ๊ณต    โณ ์žฌ์ƒ์„ฑ ์ง„ํ–‰ ์ค‘
         ๋ฐฉ๋ฌธ          (0.1์ดˆ)

02:10    ์žฌ์ƒ์„ฑ ์™„๋ฃŒ    -              โœ… ์บ์‹œ ์—…๋ฐ์ดํŠธ
                                      HTML v2 ์ค€๋น„ ์™„๋ฃŒ

02:15    ์‚ฌ์šฉ์ž E      HTML v2 ์ œ๊ณต    โœ… ์ƒˆ ๋ฒ„์ „!
         ๋ฐฉ๋ฌธ          (0.1์ดˆ)

03:00    ์‚ฌ์šฉ์ž F      HTML v2 ์ œ๊ณต    โœ… ์บ์‹œ ํžˆํŠธ
         ๋ฐฉ๋ฌธ          (0.1์ดˆ)

 

ISR ๋‚ด๋ถ€ ์บ์‹œ ๋ฉ”์ปค๋‹ˆ์ฆ˜

// Next.js ๋‚ด๋ถ€ ์บ์‹œ ๊ตฌ์กฐ (๋‹จ์ˆœํ™”)
const cache = {
  '/blog/post-1': {
    html: '<article>...</article>',
    timestamp: 1704067200000,      // ์ƒ์„ฑ ์‹œ๊ฐ
    revalidateAfter: 60,            // ์žฌ๊ฒ€์ฆ ๊ฐ„๊ฒฉ (์ดˆ)
    isRevalidating: false           // ์žฌ์ƒ์„ฑ ์ค‘์ธ์ง€
  }
};

// ์š”์ฒญ ์ฒ˜๋ฆฌ ๋กœ์ง
async function handleRequest(path) {
  const cached = cache[path];
  const now = Date.now();
  const age = (now - cached.timestamp) / 1000; // ์ดˆ ๋‹จ์œ„

  // 1. ์บ์‹œ๊ฐ€ ์œ ํšจํ•œ ๊ฒฝ์šฐ
  if (age < cached.revalidateAfter) {
    console.log('โœ… ์บ์‹œ ํžˆํŠธ: ์‹ ์„ ํ•œ ๋ฐ์ดํ„ฐ');
    return cached.html;
  }

  // 2. ์บ์‹œ๊ฐ€ ๋งŒ๋ฃŒ๋˜์—ˆ์ง€๋งŒ ์žฌ์ƒ์„ฑ ์ค‘์ด ์•„๋‹Œ ๊ฒฝ์šฐ
  if (!cached.isRevalidating) {
    console.log('๐Ÿ”„ Stale-While-Revalidate: ์˜ค๋ž˜๋œ ์บ์‹œ ์ œ๊ณต + ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์žฌ์ƒ์„ฑ');

    // ์˜ค๋ž˜๋œ ์บ์‹œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜
    const staleResponse = cached.html;

    // ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์žฌ์ƒ์„ฑ
    cached.isRevalidating = true;
    regeneratePage(path).then(newHtml => {
      cache[path] = {
        html: newHtml,
        timestamp: Date.now(),
        revalidateAfter: 60,
        isRevalidating: false
      };
      console.log('โœ… ์žฌ์ƒ์„ฑ ์™„๋ฃŒ: ์บ์‹œ ์—…๋ฐ์ดํŠธ๋จ');
    });

    return staleResponse;
  }

  // 3. ์ด๋ฏธ ์žฌ์ƒ์„ฑ ์ค‘์ธ ๊ฒฝ์šฐ
  console.log('โณ ์žฌ์ƒ์„ฑ ์ง„ํ–‰ ์ค‘: ์˜ค๋ž˜๋œ ์บ์‹œ ์ œ๊ณต');
  return cached.html;
}

 

 

ISR์˜ Stale-While-Revalidate ์ „๋žต

ISR์˜ ํ•ต์‹ฌ์€ "์˜ค๋ž˜๋œ ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๊ณตํ•˜๋ฉด์„œ ๋™์‹œ์— ์ƒˆ ์ฝ˜ํ…์ธ ๋ฅผ ์ค€๋น„"ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์š”์ฒญ → ์บ์‹œ ํ™•์ธ
       โ”œโ”€ ์‹ ์„ ํ•จ → ์ฆ‰์‹œ ๋ฐ˜ํ™˜ โœ…
       โ””โ”€ ์˜ค๋ž˜๋จ → ์˜ค๋ž˜๋œ ๊ฒƒ ๋ฐ˜ํ™˜ + ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๊ฐฑ์‹  ๐Ÿ”„
                   (๋‹ค์Œ ์š”์ฒญ๋ถ€ํ„ฐ ์ƒˆ ๋ฒ„์ „)

 

On-Demand Revalidation

์ˆ˜๋™์œผ๋กœ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const path = request.nextUrl.searchParams.get('path');

  if (path) {
    // ํŠน์ • ๊ฒฝ๋กœ ์žฌ๊ฒ€์ฆ
    revalidatePath(path);
    console.log(`๐Ÿ”„ ์ˆ˜๋™ ์žฌ๊ฒ€์ฆ: ${path}`);
    return Response.json({ revalidated: true });
  }

  return Response.json({ revalidated: false });
}

// ์‚ฌ์šฉ ์˜ˆ:
// POST /api/revalidate?path=/blog/post-1

 

 

ISR ๋‚ด๋ถ€ ๋™์ž‘:

// revalidatePath ๋‚ด๋ถ€ ๋กœ์ง
function revalidatePath(path) {
  // 1. ์บ์‹œ์—์„œ ํ•ด๋‹น ๊ฒฝ๋กœ ์ฐพ๊ธฐ
  const cached = cache[path];

  if (cached) {
    // 2. ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ๊ณผ๊ฑฐ๋กœ ์„ค์ • (๋งŒ๋ฃŒ์‹œํ‚ด)
    cached.timestamp = 0;
    console.log(`โœ… ${path} ์บ์‹œ ๋ฌดํšจํ™”๋จ`);

    // 3. ๋‹ค์Œ ์š”์ฒญ ์‹œ ์ž๋™์œผ๋กœ ์žฌ์ƒ์„ฑ๋จ
  }
}

 


3.  SSR (Server-Side Rendering) ์ž‘๋™์›๋ฆฌ

์ „์ฒด ํ๋ฆ„๋„

์‚ฌ์šฉ์ž ์š”์ฒญ
    ↓
Next.js ์„œ๋ฒ„
    ↓
๋ฐ์ดํ„ฐ ํŽ˜์นญ (API ํ˜ธ์ถœ)
    ↓
React ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง
    ↓
HTML ์ƒ์„ฑ
    ↓
๋ธŒ๋ผ์šฐ์ €๋กœ ์ „์†ก
    ↓
Hydration

 

์ƒ์„ธ ์ž‘๋™ ๊ณผ์ •

์š”์ฒญ๋ณ„ ์ฒ˜๋ฆฌ ํ๋ฆ„

// app/dashboard/page.tsx
export default async function Dashboard() {
  console.log('๐Ÿ–ฅ๏ธ  ์„œ๋ฒ„์—์„œ ์‹คํ–‰ ์ค‘...');

  const data = await fetch('<https://api.example.com/user>', {
    cache: 'no-store', // SSR ํ™œ์„ฑํ™”
    headers: {
      cookie: cookies().toString() // ์ฟ ํ‚ค ํฌํ•จ
    }
  }).then(res => res.json());

    return (
    <div>
      <h1>ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค, {data.name}๋‹˜</h1>
      <p>๋งˆ์ง€๋ง‰ ๋กœ๊ทธ์ธ: {data.lastLogin}</p>
    </div>
  );
}
 

SSR ํƒ€์ž„๋ผ์ธ ๋ถ„์„

์‚ฌ์šฉ์ž A๊ฐ€ /dashboard ์š”์ฒญ (10:00:00)
โ”‚
โ”œโ”€ [10:00:00.000] ์š”์ฒญ ๋„์ฐฉ
โ”‚   โ””โ”€ ์„œ๋ฒ„: "์ƒˆ ์š”์ฒญ ๋ฐ›์Œ"
โ”‚
โ”œโ”€ [10:00:00.050] ์ฟ ํ‚ค/ํ—ค๋” ํŒŒ์‹ฑ
โ”‚   โ””โ”€ ์„œ๋ฒ„: "์ธ์ฆ ํ† ํฐ ํ™•์ธ"
โ”‚
โ”œโ”€ [10:00:00.100] API ํ˜ธ์ถœ ์‹œ์ž‘
โ”‚   โ””โ”€ ์„œ๋ฒ„ → API: "์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์š”์ฒญ"
โ”‚
โ”œโ”€ [10:00:00.500] API ์‘๋‹ต ๋Œ€๊ธฐ ์ค‘...
โ”‚   โ””โ”€ ์„œ๋ฒ„: "์‘๋‹ต ๋Œ€๊ธฐ ์ค‘"
โ”‚
โ”œโ”€ [10:00:00.800] API ์‘๋‹ต ๋„์ฐฉ
โ”‚   โ””โ”€ ์„œ๋ฒ„: "๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์™„๋ฃŒ"
โ”‚
โ”œโ”€ [10:00:00.850] React ๋ Œ๋”๋ง
โ”‚   โ””โ”€ ์„œ๋ฒ„: "์ปดํฌ๋„ŒํŠธ๋ฅผ HTML๋กœ ๋ณ€ํ™˜"
โ”‚
โ”œโ”€ [10:00:00.900] HTML ์ƒ์„ฑ ์™„๋ฃŒ
โ”‚   โ””โ”€ ์„œ๋ฒ„: "์ตœ์ข… HTML ์ค€๋น„"
โ”‚
โ””โ”€ [10:00:00.950] ๋ธŒ๋ผ์šฐ์ €๋กœ ์ „์†ก
    โ””โ”€ ์‚ฌ์šฉ์ž: "ํŽ˜์ด์ง€ ํ‘œ์‹œ"

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

์‚ฌ์šฉ์ž B๊ฐ€ /dashboard ์š”์ฒญ (10:00:01)
โ”‚
โ””โ”€ ์œ„ ๊ณผ์ • ๋ฐ˜๋ณต (์บ์‹œ ์—†์Œ!)
   โ””โ”€ ์ด ์†Œ์š”์‹œ๊ฐ„: ~1์ดˆ

 

์„œ๋ฒ„ ๋‚ด๋ถ€ ์ฒ˜๋ฆฌ

// Next.js ์„œ๋ฒ„์˜ ์š”์ฒญ ํ•ธ๋“ค๋Ÿฌ (๋‹จ์ˆœํ™”)
async function handleSSRRequest(req, res) {
  console.log('๐Ÿ–ฅ๏ธ  SSR ์š”์ฒญ ์ฒ˜๋ฆฌ ์‹œ์ž‘');
  const startTime = Date.now();

  try {
    // 1. ๋ผ์šฐํŠธ ๋งค์นญ
    const route = matchRoute(req.url);
    console.log(`๐Ÿ“ ๋ผ์šฐํŠธ: ${route.path}`);

    // 2. ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
    const PageComponent = await import(route.componentPath);

    // 3. ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์‹คํ–‰ (๋ฐ์ดํ„ฐ ํŽ˜์นญ ํฌํ•จ)
    console.log('๐Ÿ”„ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ์‹œ์ž‘...');
    const props = await PageComponent.getServerSideProps?.(req, res);

    // 4. React ๋ Œ๋”๋ง
    console.log('๐ŸŽจ React ๋ Œ๋”๋ง ์‹œ์ž‘...');
    const html = ReactDOMServer.renderToString(
      <PageComponent {...props} />
    );

    // 5. HTML ํ…œํ”Œ๋ฆฟ์— ์‚ฝ์ž…
    const fullHtml = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>${props.title}</title>
          <link rel="stylesheet" href="/styles.css" />
        </head>
        <body>
          <div id="root">${html}</div>
          <script src="/runtime.js"></script>
          <script>
            window.__INITIAL_DATA__ = ${JSON.stringify(props)};
          </script>
        </body>
      </html>
    `;

    const endTime = Date.now();
    console.log(`โœ… ๋ Œ๋”๋ง ์™„๋ฃŒ (${endTime - startTime}ms)`);

    // 6. ์‘๋‹ต ์ „์†ก
    res.setHeader('Content-Type', 'text/html');
    res.send(fullHtml);

  } catch (error) {
    console.error('โŒ SSR ์—๋Ÿฌ:', error);
    res.status(500).send('Server Error');
  }
}

 

SSR ์ธ์ฆ/์ฟ ํ‚ค ์ฒ˜๋ฆฌ

// app/profile/page.tsx
import { cookies, headers } from 'next/headers';

export default async function Profile() {
  // 1. ์ฟ ํ‚ค ์ฝ๊ธฐ
  const cookieStore = cookies();
  const token = cookieStore.get('auth_token');

  console.log('๐Ÿ” ์ธ์ฆ ํ† ํฐ:', token?.value);

  if (!token) {
    redirect('/login');
  }

  // 2. ํ—ค๋” ์ฝ๊ธฐ
  const headersList = headers();
  const userAgent = headersList.get('user-agent');

  console.log('๐Ÿ‘ค User-Agent:', userAgent);

  // 3. ์ธ์ฆ๋œ API ํ˜ธ์ถœ
  const user = await fetch('<https://api.example.com/profile>', {
    headers: {
      'Authorization': `Bearer ${token.value}`,
      'User-Agent': userAgent || ''
    },
    cache: 'no-store'
  }).then(res => res.json());

  return

 

 

์„ฑ๋Šฅ ์ตœ์ ํ™”: Streaming SSR

// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <h1>๋Œ€์‹œ๋ณด๋“œ</h1>

      {/* ์ฆ‰์‹œ ๋ Œ๋”๋ง */}
      <QuickStats />

      {/* ๋А๋ฆฐ ๋ถ€๋ถ„์€ ๋‚˜์ค‘์— */}
      <Suspense fallback={<Loading />}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

async function SlowComponent() {
  // 3์ดˆ ๊ฑธ๋ฆฌ๋Š” API ํ˜ธ์ถœ
  const data = await fetch('https://slow-api.com/data', {
    cache: 'no-store'
  });

  return <HeavyChart data={data} />;
}
 

Streaming ๋™์ž‘:

ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†ก๋˜๋Š” HTML:

[0.1์ดˆ]
<div>
  <h1>๋Œ€์‹œ๋ณด๋“œ</h1>
  <div>๋น ๋ฅธ ํ†ต๊ณ„: ...</div>
  <div id="suspense-boundary">๋กœ๋”ฉ ์ค‘...</div>
</div>

[3.1์ดˆ - ์ถ”๊ฐ€ ์ฒญํฌ]
<script>
  // Suspense ๊ฒฝ๊ณ„ ์—…๋ฐ์ดํŠธ
  replaceSuspenseBoundary('suspense-boundary', `
    <div>๋А๋ฆฐ ์ปดํฌ๋„ŒํŠธ ๋‚ด์šฉ...</div>
  `);
</script>

 

4. CSR (Client-Side Rendering) ์ž‘๋™์›๋ฆฌ

 ์ „์ฒด ํ๋ฆ„๋„

์‚ฌ์šฉ์ž ์š”์ฒญ
    ↓
๋นˆ HTML + JavaScript ๋ฒˆ๋“ค ์ „์†ก
    ↓
๋ธŒ๋ผ์šฐ์ €์—์„œ JavaScript ์‹คํ–‰
    ↓
React ์•ฑ ์ดˆ๊ธฐํ™”
    ↓
API ํ˜ธ์ถœ (fetch/axios)
    ↓
์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง
    ↓
ํ™”๋ฉด์— ํ‘œ์‹œ

 

์ƒ์„ธ ์ž‘๋™ ๊ณผ์ •

์ดˆ๊ธฐ HTML (๊ฑฐ์˜ ๋น„์–ด์žˆ์Œ)

<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <!-- ๊ฑฐ์˜ ๋นˆ ๊ป๋ฐ๊ธฐ -->
  <div id="root"></div>

  <!-- JavaScript ๋ฒˆ๋“ค๋งŒ ๋กœ๋“œ -->
  <script src="/static/js/runtime.js"></script>
  <script src="/static/js/vendors.js"></script>
  <script src="/static/js/main.js"></script>
</body>
</html>

 

JavaScript ์‹คํ–‰ ํ๋ฆ„

// app/dashboard/page.tsx
'use client'; // CSR ๋ช…์‹œ

import { useState, useEffect } from 'react';

export default function Dashboard() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    console.log('๐ŸŒ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰ ์ค‘...');
    console.log('๐Ÿ“ก API ํ˜ธ์ถœ ์‹œ์ž‘...');

    fetch('/api/user')
      .then(res => {
        console.log('โœ… API ์‘๋‹ต ๋ฐ›์Œ');
        return res.json();
      })
      .then(data => {
        console.log('๐Ÿ“ฆ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์™„๋ฃŒ');
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('โŒ ์—๋Ÿฌ:', error);
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <div>๋กœ๋”ฉ ์ค‘...</div>;
  }

  return (
    <div>
      <h1>ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค, {user.name}๋‹˜!</h1>
    </div>
  );
}

CSR ํƒ€์ž„๋ผ์ธ ๋ถ„์„

[0.0์ดˆ] ์‚ฌ์šฉ์ž๊ฐ€ /dashboard ์š”์ฒญ
โ”‚
โ”œโ”€ [0.1์ดˆ] HTML ๋‹ค์šด๋กœ๋“œ (5KB)
โ”‚   โ””โ”€ ํ™”๋ฉด: ๋นˆ ํŽ˜์ด์ง€
โ”‚
โ”œโ”€ [0.2์ดˆ] JavaScript ๋‹ค์šด๋กœ๋“œ ์‹œ์ž‘
โ”‚   โ”œโ”€ runtime.js (50KB)
โ”‚   โ”œโ”€ vendors.js (200KB - React ๋“ฑ)
โ”‚   โ””โ”€ main.js (100KB - ์•ฑ ์ฝ”๋“œ)
โ”‚   โ””โ”€ ํ™”๋ฉด: ์—ฌ์ „ํžˆ ๋นˆ ํŽ˜์ด์ง€
โ”‚
โ”œโ”€ [1.0์ดˆ] JavaScript ํŒŒ์‹ฑ & ์‹คํ–‰
โ”‚   โ””โ”€ ํ™”๋ฉด: ์—ฌ์ „ํžˆ ๋นˆ ํŽ˜์ด์ง€
โ”‚
โ”œโ”€ [1.2์ดˆ] React ์•ฑ ์ดˆ๊ธฐํ™”
โ”‚   โ””โ”€ ReactDOM.render(<App />)
โ”‚   โ””โ”€ ํ™”๋ฉด: "๋กœ๋”ฉ ์ค‘..." ํ‘œ์‹œ
โ”‚
โ”œโ”€ [1.3์ดˆ] useEffect ์‹คํ–‰
โ”‚   โ””โ”€ fetch('/api/user') ํ˜ธ์ถœ
โ”‚   โ””โ”€ ํ™”๋ฉด: "๋กœ๋”ฉ ์ค‘..."
โ”‚
โ”œโ”€ [1.8์ดˆ] API ์‘๋‹ต ๋„์ฐฉ
โ”‚   โ””โ”€ ํ™”๋ฉด: "๋กœ๋”ฉ ์ค‘..."
โ”‚
โ””โ”€ [2.0์ดˆ] setState ์™„๋ฃŒ & ๋ฆฌ๋ Œ๋”๋ง
    โ””โ”€ ํ™”๋ฉด: ์‹ค์ œ ์ฝ˜ํ…์ธ  ํ‘œ์‹œ โœ…

 

CSR ๋ธŒ๋ผ์šฐ์ € ๋‚ด๋ถ€ ์ฒ˜๋ฆฌ

// React์˜ CSR ์ดˆ๊ธฐํ™” (๋‹จ์ˆœํ™”)
window.addEventListener('DOMContentLoaded', () => {
  console.log('๐Ÿ“„ DOM ๋กœ๋“œ ์™„๋ฃŒ');

  // 1. React ๋ฃจํŠธ ์ƒ์„ฑ
  const root = ReactDOM.createRoot(document.getElementById('root'));
  console.log('๐ŸŒฑ React ๋ฃจํŠธ ์ƒ์„ฑ');

  // 2. ์•ฑ ๋ Œ๋”๋ง
  console.log('๐ŸŽจ ์•ฑ ๋ Œ๋”๋ง ์‹œ์ž‘');
  root.render(<App />);

  // 3. ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ
  // App → Dashboard → useEffect ์ˆœ์„œ๋กœ ์‹คํ–‰
});

 

CSR ์ƒํƒœ ๊ด€๋ฆฌ์™€ ๋ฆฌ๋ Œ๋”๋ง

'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  console.log('๐Ÿ”„ Counter ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง:', count);

  const increment = () => {
    console.log('โž• ์ฆ๊ฐ€ ๋ฒ„ํŠผ ํด๋ฆญ');
    setCount(prev => {
      const newCount = prev + 1;
      console.log(`์ƒํƒœ ๋ณ€๊ฒฝ: ${prev} → ${newCount}`);
      return newCount;
    });
    // ์ƒํƒœ ๋ณ€๊ฒฝ ํ›„ ์ž๋™์œผ๋กœ ๋ฆฌ๋ Œ๋”๋ง ํŠธ๋ฆฌ๊ฑฐ
  };

  return (
    <div>
      <p>์นด์šดํŠธ: {count}</p>
      <button onClick={increment}>์ฆ๊ฐ€</button>
    </div>
  );
}

 

์‹คํ–‰ ํ๋ฆ„:

[์ดˆ๊ธฐ ๋ Œ๋”๋ง]
โ””โ”€ ๐Ÿ”„ Counter ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง: 0

[๋ฒ„ํŠผ ํด๋ฆญ]
โ”œโ”€ โž• ์ฆ๊ฐ€ ๋ฒ„ํŠผ ํด๋ฆญ
โ”œโ”€ ์ƒํƒœ ๋ณ€๊ฒฝ: 0 → 1
โ””โ”€ ๐Ÿ”„ Counter ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง: 1
   โ””โ”€ Virtual DOM ์ƒ์„ฑ
      โ””โ”€ ์‹ค์ œ DOM๊ณผ ๋น„๊ต (Reconciliation)
         โ””โ”€ ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ์—…๋ฐ์ดํŠธ
            โ””โ”€ <p>์นด์šดํŠธ: 1</p> โœ…

 

React์˜ Reconciliation (์žฌ์กฐ์ •)

// React ๋‚ด๋ถ€ ๋กœ์ง (๋‹จ์ˆœํ™”)
function updateComponent(Component, newProps) {
  // 1. ์ƒˆ๋กœ์šด Virtual DOM ์ƒ์„ฑ
  const newVDOM = Component(newProps);
  console.log('๐Ÿ†• ์ƒˆ Virtual DOM:', newVDOM);

  // 2. ์ด์ „ Virtual DOM๊ณผ ๋น„๊ต
  const diff = compareVDOM(oldVDOM, newVDOM);
  console.log('๐Ÿ” ๋ณ€๊ฒฝ ์‚ฌํ•ญ:', diff);

  // 3. ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ์‹ค์ œ DOM ์—…๋ฐ์ดํŠธ
  diff.forEach(change => {
    if (change.type === 'UPDATE_TEXT') {
      const element = document.querySelector(change.selector);
      element.textContent = change.newValue;
      console.log(`โœ๏ธ  ํ…์ŠคํŠธ ์—…๋ฐ์ดํŠธ: ${change.oldValue} → ${change.newValue}`);
    }
  });

  // 4. ์ด์ „ VDOM์„ ์ƒˆ VDOM์œผ๋กœ ๊ต์ฒด
  oldVDOM = newVDOM;
}

 

 

 

 

์›๋ฌธ

https://tkdodo.eu/blog/deriving-client-state-from-server-state

 

Deriving Client State from Server State

How to use derived state in React to keep client state and server data aligned without manual sync or effects.

tkdodo.eu

 

 

ํœด๊ฐ€์—์„œ ๋ง‰ ๋Œ์•„์™”๋Š”๋ฐ, Zustand๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๊ฐ€์žฅ ํฐ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„์— ๋Œ€ํ•œ Reddit ์งˆ๋ฌธ์„ ๋ณด๊ฒŒ ๋˜์—ˆ๋‹ค.
์ฝ”๋“œ๋Š” ๋Œ€๋žต ์ด๋ ‡๋‹ค(์ตœ์‹  ๋ฌธ๋ฒ•์œผ๋กœ ์กฐ๊ธˆ ์ˆ˜์ •ํ•˜๊ณ , ์ปค์Šคํ…€ ํ›… ์•ˆ์— ๋„ฃ์—ˆ๋‹ค).

 

Manual-sync

const useSelectedUser = () => {
  const { data: users } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })
  const { selectedUserId, setSelectedUserId } = useUserStore()

  // If the selected user gets deleted from the server,
  // Zustand won't automatically clear selectedUserId
  // You have to manually handle this:
  useEffect(() => {
    if (!users?.some((u) => u.id === selectedUserId)) {
      setSelectedUserId(null) // Manual sync required
    }
  }, [users, selectedUserId])

  return [selectedUserId, selectedUserId]
}

 

๋ฌผ๋ก , useEffect—ํŠนํžˆ ๊ทธ ์•ˆ์—์„œ setState๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ—๋ฅผ ๋ณผ ๋•Œ๋งˆ๋‹ค ๋‚˜๋Š” ๋” ๋‚˜์€ ํ•ด๊ฒฐ์ฑ…์ด ์—†์„๊นŒ ์ƒ๊ฐํ•˜๊ฒŒ ๋œ๋‹ค.
๋‚ด ๊ฒฝํ—˜์ƒ, ๊ฑฐ์˜ ํ•ญ์ƒ ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์ด ์กด์žฌํ•˜๋ฉฐ, ๋ณดํ†ต ๊ทธ ๋ฐฉ๋ฒ•์„ ์ฐพ๋Š” ๊ฒƒ์ด ๊ฐ€์น˜ ์žˆ๋Š” ์ผ์ด๋‹ค.
๊ทธ๋Ÿฌ๋‹ˆ ํ•œ ๊ฑธ์Œ ๋ฌผ๋Ÿฌ์„œ์„œ ์šฐ๋ฆฌ๊ฐ€ ๋จผ์ € ๋ฌด์—‡์„ ๋‹ฌ์„ฑํ•˜๊ณ  ์‹ถ์€์ง€๋ฅผ ๋จผ์ € ์‚ดํŽด๋ณด์ž.

 

 

์ƒํƒœ ๋™๊ธฐํ™” ์œ ์ง€ํ•˜๊ธฐ (Keeping State in Sync)

๊ฐ„๋‹จํžˆ ๋งํ•˜๋ฉด, ์šฐ๋ฆฌ๋Š” ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ(Client State)์ธ selectedUserId๋ฅผ ์„œ๋ฒ„ ์ƒํƒœ(Server State)์ธ ์‚ฌ์šฉ์ž ๋ชฉ๋ก(users)๊ณผ ๋™๊ธฐํ™”ํ•˜๊ณ  ์‹ถ๋‹ค.
์ด๊ฒƒ์€ ๋‹น์—ฐํ•œ ์ผ์ด๋‹ค.


์˜ˆ๋ฅผ ๋“ค์–ด, useQuery๋ฅผ ํ†ตํ•ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋‹ค์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™”๋Š”๋ฐ, ์‚ฌ์šฉ์ž๊ฐ€ ๋ชฉ๋ก์—์„œ ์‚ญ์ œ๋˜์—ˆ์Œ์—๋„ ์—ฌ์ „ํžˆ ์ƒํƒœ์— ์ €์žฅ๋˜์–ด ์žˆ๋‹ค๋ฉด, ๊ทธ ์„ ํƒ์€ ์œ ํšจํ•˜์ง€ ์•Š๋‹ค.

 

 

ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ(Client State)

์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ Zustand์—๋งŒ ๊ตญํ•œ๋œ ๊ฒƒ์ด ์•„๋‹ˆ๋‹ค.
useUserStore()๋ฅผ ๋‹จ์ˆœํžˆ ๋กœ์ปฌ ์ƒํƒœ๋กœ ๋ฐ”๊ฟ”๋„ ๋˜‘๊ฐ™์ด ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค:

const [selectedUserId, setSelectedUserId] = useState()

๋˜๋Š” URL ์ƒํƒœ(nuqs ๊ฐ™์€)๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค:

const [selectedUserId, setSelectedUserId] = useQueryState('userId')

์ค‘์š”ํ•œ ๊ฑด ์ƒํƒœ๊ฐ€ ์–ด๋””์— ์ €์žฅ๋˜๋А๋ƒ๊ฐ€ ์•„๋‹ˆ๋ผ,
์„œ๋ฒ„ ์ƒํƒœ(Server State)๊ฐ€ ๋ฐ”๋€” ๋•Œ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ(Client State)๋ผ๋Š” ์ ์ด๋‹ค.

 

 

์ฟผ๋ฆฌ(Query)์—๋Š” onSuccess ์ฝœ๋ฐฑ์ด ์—†๊ณ ,
๋ Œ๋” ์ค‘์— setState๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ํŠธ๋ฆญ์€ React์˜ ๋‚ด์žฅ ์ƒํƒœ์—์„œ๋งŒ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์—,
์‚ฌ์‹ค์ƒ ๋‚จ์€ ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์€ ๋ฌด์‹œ๋ฌด์‹œํ•œ useEffect๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

 

๊ฒฐ๊ตญ user selection์„ ์—…๋ฐ์ดํŠธํ•˜๋ ค๋ฉด ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์ด ์žˆ์„๊นŒ?

 

์ƒํƒœ๋ฅผ ๋™๊ธฐํ™”ํ•˜์ง€ ๋ง๊ณ , ํŒŒ์ƒํ•˜๋ผ(Don’t Sync State – Derive It)

Kent C. Dodds๊ฐ€ ์“ด ๊ธ€์„ ๊ธฐ์–ตํ•˜๋Š”๊ฐ€?
๊ทธ๋Š” ๋„ค ๊ฐœ์˜ ์„œ๋กœ ๋‹ค๋ฅธ useState๋ฅผ ํ•˜๋‚˜๋กœ ์ค„์ด๊ณ , ๋‚˜๋จธ์ง€๋Š” ๋‹จ์ผ ์ง„์‹ค์˜ ์ถœ์ฒ˜(source of truth)์—์„œ ํŒŒ์ƒํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ฃผ์—ˆ๋‹ค.

์šฐ๋ฆฌ ์ƒํ™ฉ์—์„œ๋„ ๋น„์Šทํ•œ ์ ‘๊ทผ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.
useEffect๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์†Œ ๋ช…๋ นํ˜•(imperative) ์‚ฌ๊ณ  ๋ฐฉ์‹์ด๋‹ค:

์‚ฌ์šฉ์ž๊ฐ€ ๋ณ€๊ฒฝ๋˜๊ณ , ํ˜„์žฌ ์„ ํƒ์ด ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด, ์„ ํƒ์„ null๋กœ ์žฌ์„ค์ •ํ•œ๋‹ค.

ํ•˜์ง€๋งŒ ์ด ์‚ฌ๊ณ  ๋ฐฉ์‹์„ ์กฐ๊ธˆ ๋” ์„ ์–ธํ˜•(declarative)์œผ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค:

์—ฌ๊ธฐ ๋ฐฑ์—”๋“œ์—์„œ ๊ฐ€์ ธ์˜จ Users์™€ Current selection์ด ์žˆ๋‹ค.
์ด ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์‹ค์ œ ์ƒํƒœ(real state)๋ฅผ ๋‹ฌ๋ผ.

 

 

const useSelectedUser = () => {
  const { data: users } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })
  const { selectedUserId, setSelectedUserId } = useUserStore()

  const selectedId = users?.some((u) => u.id === selectedUserId)
    ? selectedUserId
    : null

  return [selectedId, setSelectedUserId]
}

 

์ด ์ฝ”๋“œ๋Š” ๋งค์šฐ ๊ฐ„๋‹จํ•˜๋‹ค.
์Šคํ† ์–ด ๊ฐ’์„ ์ง์ ‘ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋Œ€์‹ , Selection ์ž์ฒด๋Š” ๊ทธ๋Œ€๋กœ ๋‘๊ณ ,
์„œ๋ฒ„ ์ƒํƒœ(Server State)์—์„œ ๋” ์ด์ƒ ํ•ด๋‹น id๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ ์ปค์Šคํ…€ ํ›…์—์„œ ๋‹ค๋ฅธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•œ๋‹ค.


์ฆ‰, useSelectedUser()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ณณ์—์„œ๋Š” ์ด์ „์ฒ˜๋Ÿผ null์„ ๋ฐ›๊ฒŒ ๋œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์Šคํ† ์–ด๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ถ”๊ฐ€์ ์ธ ์ด์ ๋„ ์žˆ๋‹ค:

  • ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค์‹œ ๋ชฉ๋ก์— ์ถ”๊ฐ€๋˜๋ฉด, ์„ ํƒ(selection)์ด ์ž๋™์œผ๋กœ ๋ณต์›๋œ๋‹ค.
  • UX ์š”๊ตฌ์‚ฌํ•ญ์ด ๋ฐ”๋€Œ์–ด ์„ ํƒ์„ ์ œ๊ฑฐํ•˜๊ณ  ์‹ถ์ง€ ์•Š๊ณ , ๋‹จ์ง€ ์„ ํƒ์ด ์œ ํšจํ•˜์ง€ ์•Š์Œ์„ ์‹œ๊ฐ์ ์œผ๋กœ ํ‘œ์‹œํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.
    → ์›๋ž˜ ๊ฐ’์„ ํ•ญ์ƒ ์œ ์ง€ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

isSelectionValid

const useSelectedUser = () => {
  const { data: users } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })
  const { selectedUserId, setSelectedUserId } = useUserStore()
  const isSelectionValid = users?.some((u) => u.id === selectedUserId)

  return [selectedUserId, setSelectedUserId, isSelectionValid]
}

 

ํ•จ์ •์€ ์–ด๋””์— ์žˆ์„๊นŒ์š”?

"์ƒํƒœ ํŒŒ์ƒ ์†”๋ฃจ์…˜"์˜ ๋ช…๋ฐฑํ•œ ๋‹จ์  ์ค‘ ํ•˜๋‚˜๋Š” ์‚ฌ์šฉ์ž ์ €์žฅ์†Œ์— ์ €์žฅ๋œ ๋‚ด์šฉ์„ ๋” ์ด์ƒ "์‹ ๋ขฐ"ํ•  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. useUserStore์—์„œ ๋‹ค๋ฅธ ๊ณณ์˜ selectedUserId๋ฅผ ์ฝ์–ด์˜ค๋ฉด ์ถ”๊ฐ€ ๊ฒ€์‚ฌ๋ฅผ ๋ฐ›์ง€ ๋ชปํ•˜๋ฏ€๋กœ ํ•ญ์ƒ ์‚ฌ์šฉ์ž ์ •์˜ hook์—์„œ ์ฝ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ €๋Š” ์ €์žฅ์†Œ๋ฅผ ์ตœ์ข… ๊ฒ€์ฆ๋œ ๊ฐ’์˜ ์†Œ์Šค๋ผ๊ธฐ๋ณด๋‹ค๋Š” UI์—์„œ ์‹ค์ œ๋กœ ์„ ํƒ๋œ ํ•ญ๋ชฉ์˜ ๊ธฐ๋ก์œผ๋กœ ๋ณด๊ธฐ ๋•Œ๋ฌธ์— ์ด ์ ์— ๋Œ€ํ•ด์„œ๋Š” ์ „ํ˜€ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

๋˜, Reddit ์งˆ๋ฌธ์—์„œ Redux Toolkit์ด “์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•œ๋‹ค”๊ณ  ์–ธ๊ธ‰ํ–ˆ์ง€๋งŒ, ์‹ค์ œ๋กœ ํฌ๊ฒŒ ๋‹ค๋ฅด์ง€ ์•Š์„ ๊ฒƒ์ด๋‹ค.
๋ณดํ†ต API ์Šฌ๋ผ์ด์Šค์™€ ์‚ฌ์šฉ์ž ์„ ํƒ ์Šฌ๋ผ์ด์Šค๋ฅผ ์ฝ์–ด ๋‘ ๊ฐ’์„ ๊ฒฐํ•ฉํ•˜๋Š” selector๋ฅผ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜๋Š”๋ฐ,
์ด๊ฒƒ์ด ๋ฐ”๋กœ ์šฐ๋ฆฌ๊ฐ€ ์ปค์Šคํ…€ ํ›…์—์„œ ํ•˜๊ณ  ์žˆ๋Š” ๋ฐฉ์‹๊ณผ ๋™์ผํ•˜๋‹ค.

 

์˜คํžˆ๋ ค ์ด ๋ฐฉ์‹์€ ์ƒํƒœ๋ฅผ ํŒŒ์ƒ(derive)ํ•˜๋„๋ก ์œ ๋„ํ•˜๋ฏ€๋กœ, ์ข‹์€ ์ ‘๊ทผ์ž…๋‹ˆ๋‹ค.

 

๋‹ค๋ฅธ ์˜ˆ์‹œ

์„œ๋ฒ„ ์ƒํƒœ(Server State)๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ(Client State)๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š๋Š” ๊ฐœ๋…์€ ์—ฌ๋Ÿฌ ๊ฒฝ์šฐ์— ์œ ์šฉํ•˜๋‹ค.
ํ”ํ•œ ์˜ˆ์‹œ๋Š” ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜จ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ํผ์„ ๋ฏธ๋ฆฌ ์ฑ„์šธ ๋•Œ(prefilling forms)์ด๋‹ค.

 

default-value-effect

function UserSelection() {
  const { data: users } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })
  const [selection, setSelection] = useState()

  // use the first value as default selection
  useEffect(() => {
    if (users?.[0]) {
      setSelection(users[0])
    }
  }, [users])

  // return markup
}

 

์ด ํšจ๊ณผ๋Š” ์žฅํ™ฉํ•  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ์ฟผ๋ฆฌ์—์„œ ์ƒˆ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ฌ ๋•Œ ํ˜„์žฌ ์„ ํƒ ํ•ญ๋ชฉ์„ ๋ฎ์–ด์“ฐ๋Š” ๋ฒ„๊ทธ๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ›

์ด ๋ฌธ์ œ๋Š” ๋‹ค๋ฅธ ๊ฒ€์‚ฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์‰ฝ๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๋” ๋‚˜์€ ํ•ด๊ฒฐ์ฑ…์€ ์—ฌ์ „ํžˆ โ€‹โ€‹์ƒํƒœ๋ฅผ ํŒŒ์ƒํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

 

derived-default-value

function UserSelection() {
  const { data: users } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })
  const [selection, setSelection] = useState()

  const derivedSelection = selection ?? users?.[0]

  // return markup
}

 

 

์ง€๊ธˆ ์šฐ๋ฆฌ๊ฐ€ ํ•ด์•ผ ํ•  ์ผ์€ selection ๋Œ€์‹  derivedSelection์„ ๊ณ„์† ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋ฟ์ด๋ฉฐ, ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ํ•ญ์ƒ ์›ํ•˜๋Š” ๊ฐ’์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿš€

 

 

https://tkdodo.eu/blog/the-useless-use-callback

 

The Useless useCallback

Why most memoization is downright useless...

tkdodo.eu

์›๋ฌธ

 

 

๋ฉ”๋ชจ์ด์ œ์ด์…˜(memoization)์— ๋Œ€ํ•ด์„œ๋Š” ์ด์ œ ์ถฉ๋ถ„ํžˆ ๋‹ค๋ค˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋Š”๋ฐ, ์š”์ฆ˜ ์ž์ฃผ ๋ณด์ด๋Š” ํ•œ ๊ฐ€์ง€ ํŒจํ„ด์„ ๋ณด๋ฉด์„œ ์ƒ๊ฐ์ด ๋‹ฌ๋ผ์กŒ๋‹ค.
๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ useCallback๊ณผ, ๋ถ€๋ถ„์ ์œผ๋กœ๋Š” useMemo์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณด๋ ค ํ•œ๋‹ค.
ํŠนํžˆ ๋‚ด๊ฐ€ ๋ณด๊ธฐ์—” ์ •๋ง ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ๋“ค์— ๋Œ€ํ•ด ๋ง์ด๋‹ค.

 

์™œ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์‚ฌ์šฉํ• ๊นŒ?

๋ณดํ†ต useCallback์œผ๋กœ ํ•จ์ˆ˜๋ฅผ, useMemo๋กœ ๊ฐ’์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋Š” ์ด์œ ๋Š” ๋‹จ ๋‘ ๊ฐ€์ง€๋ฟ์ด๋‹ค.

 

1. ์„ฑ๋Šฅ ์ตœ์ ํ™”(Performance optimization)

์–ด๋–ค ๋™์ž‘์ด ๋А๋ฆด ๋•Œ, ์šฐ๋ฆฌ๋Š” ๋ณดํ†ต ๊ทธ๊ฒƒ์„ “๋‚˜์˜๋‹ค”๊ณ  ๋А๋‚€๋‹ค. ์ด์ƒ์ ์œผ๋กœ๋Š” ๋” ๋น ๋ฅด๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒŒ ์ข‹์ง€๋งŒ, ํ•ญ์ƒ ๊ทธ๋ ‡๊ฒŒ ํ•  ์ˆ˜๋Š” ์—†๋‹ค. ๋Œ€์‹  ๊ทธ ๋А๋ฆฐ ๋™์ž‘์ด ์ผ์–ด๋‚˜๋Š” ํšŸ์ˆ˜๋ฅผ ์ค„์ด๋Š” ๋ฐฉ๋ฒ•์„ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๋‹ค.

React์—์„œ๋Š” ์ด๋Ÿฐ “๋А๋ฆฐ ๋™์ž‘”์ด ์ฃผ๋กœ ํ•˜์œ„ ํŠธ๋ฆฌ(sub-tree)์˜ ๋ฆฌ๋ Œ๋”๋ง์ด๋‹ค. ๋”ฐ๋ผ์„œ “๊ตณ์ด ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค”๊ณ  ํŒ๋‹จ๋˜๋ฉด ์ด๋ฅผ ํ”ผํ•˜๊ณ  ์‹ถ์–ด์ง„๋‹ค.

 

์ด๋Ÿฐ ์ด์œ ๋กœ ๊ฐ€๋” React.memo๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ์‹ธ๊ธฐ๋„ ํ•œ๋‹ค. ๋ฌผ๋ก  ์ด๋Š” ๋Œ€๋ถ€๋ถ„ “๋…ธ๋ ฅ ๋Œ€๋น„ ํšจ๊ณผ๊ฐ€ ํฌ์ง€ ์•Š์€ ์‹ธ์›€”์ด์ง€๋งŒ, ๊ทธ๋ž˜๋„ ์กด์žฌํ•˜๋Š” ํ…Œํฌ๋‹‰์ด๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ์ปดํฌ๋„ŒํŠธ์— ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜, ์›์‹œ ํƒ€์ž…์ด ์•„๋‹Œ ๊ฐ’์„ props๋กœ ๋„˜๊ธธ ๊ฒฝ์šฐ์—” ์ฐธ์กฐ(reference)๊ฐ€ ์•ˆ์ •์ ์ด์–ด์•ผ ํ•œ๋‹ค.
๊ทธ ์ด์œ ๋Š” React๊ฐ€ Object.is๋ฅผ ์‚ฌ์šฉํ•ด props๋ฅผ ๋น„๊ตํ•˜๊ณ , ๋™์ผํ•˜๋‹ค๊ณ  ํŒ๋‹จ๋˜๋ฉด ํ•ด๋‹น ํ•˜์œ„ ํŠธ๋ฆฌ์˜ ๋ฆฌ๋ Œ๋”๋ง์„ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๋งŒ์•ฝ ์ด ์ฐธ์กฐ๊ฐ€ ์•ˆ์ •์ ์ด์ง€ ์•Š๋‹ค๋ฉด — ์˜ˆ๋ฅผ ๋“ค์–ด, ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ์ƒˆ๋กญ๊ฒŒ ์ƒ์„ฑ๋˜๋Š” ๊ฐ’์ด๋ผ๋ฉด — ์šฐ๋ฆฌ์˜ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์€ ๊นจ์ง€๊ฒŒ ๋œ๋‹ค.

 

function Meh() {
  return (
    <MemoizedComponent
      value={{ hello: 'world' }}
      onChange={(result) => console.log('result')}
    />
  )
}

function Okay() {
  const value = useMemo(() => ({ hello: 'world' }), [])
  const onChange = useCallback((result) => console.log(result), [])

  return <MemoizedComponent value={value} onChange={onChange} />
}

 

๋ฌผ๋ก  ๋•Œ๋กœ๋Š” useMemo ๋‚ด๋ถ€์˜ ๊ณ„์‚ฐ ์ž์ฒด๊ฐ€ ๋А๋ฆด ๋•Œ๊ฐ€ ์žˆ๋‹ค.
์ด๋Ÿด ๋• ๋ถˆํ•„์š”ํ•œ ์žฌ๊ณ„์‚ฐ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ด๋Ÿฐ ๊ฒฝ์šฐ์˜ useMemo ์‚ฌ์šฉ์€ ์ „ํ˜€ ๋ฌธ์ œ ์—†๋‹ค.
ํ•˜์ง€๋งŒ ์†”์งํžˆ ๋งํ•˜์ž๋ฉด, ์ด๋Ÿฐ ๊ฒฝ์šฐ๊ฐ€ ๋Œ€๋ถ€๋ถ„์€ ์•„๋‹ˆ๋‹ค.

 

 

2. Effect๊ฐ€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์ž์ฃผ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด

๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ๊ฐ’์„ ๋ฉ”๋ชจ๋œ ์ปดํฌ๋„ŒํŠธ์˜ props๋กœ ๋„˜๊ธฐ์ง€ ์•Š๋”๋ผ๋„,
๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ๊ทธ ๊ฐ’์€ ๊ฒฐ๊ตญ ์–ด๋–ค Effect์˜ ์˜์กด์„ฑ(dependency) ์œผ๋กœ ์ „๋‹ฌ๋œ๋‹ค.
(๋•Œ๋กœ๋Š” ์—ฌ๋Ÿฌ ๋‹จ๊ณ„์˜ ์ปค์Šคํ…€ ํ›…์„ ๊ฑฐ์ณ์„œ ์ „๋‹ฌ๋˜๊ธฐ๋„ ํ•œ๋‹ค.)

Effect์˜ ์˜์กด์„ฑ ๋น„๊ต ๋ฐฉ์‹์€ React.memo์™€ ๋™์ผํ•˜๋‹ค.
React๋Š” ๊ฐ ์˜์กด์„ฑ์„ Object.is๋กœ ํ•˜๋‚˜์”ฉ ๋น„๊ตํ•ด, ๊ฐ’์ด ๋ฐ”๋€Œ์—ˆ๋‹ค๋ฉด Effect๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•œ๋‹ค.

๋”ฐ๋ผ์„œ Effect์˜ ์˜์กด์„ฑ ๊ฐ’์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜์ง€ ์•Š์œผ๋ฉด,
๋ Œ๋”๋ง์ด ์ผ์–ด๋‚  ๋•Œ๋งˆ๋‹ค Effect๊ฐ€ ๋งค๋ฒˆ ์žฌ์‹คํ–‰๋˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๊ฐ€๋งŒํžˆ ์ƒ๊ฐํ•ด๋ณด๋ฉด, ์ด ๋‘ ๊ฐ€์ง€ ์ƒํ™ฉ์€ ์‚ฌ์‹ค ์™„์ „ํžˆ ๊ฐ™์€ ๋งฅ๋ฝ์ด๋ผ๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
๋‘˜ ๋‹ค “๊ฐ™์€ ์ฐธ์กฐ(reference)๋ฅผ ์œ ์ง€ํ•ด์„œ ์–ด๋–ค ์ผ์ด ์ผ์–ด๋‚˜์ง€ ์•Š๋„๋ก ๋ง‰๋Š” ๊ฒƒ”์ด ๋ชฉ์ ์ด๋‹ค.

์ฆ‰, useCallback์ด๋‚˜ useMemo๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ€์žฅ ๊ณตํ†ต๋œ ์ด์œ ๋Š” ๊ฒฐ๊ตญ ์ด๊ฑฐ๋‹ค:

 

 

์ฐธ์กฐ์˜ ์•ˆ์ •์„ฑ์ด ํ•„์š”ํ•˜๋‹ค.
์ฆ‰, ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๊ฐ’์ด๋‚˜ ํ•จ์ˆ˜๊ฐ€ ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์ง€ ์•Š๊ณ  
๊ฐ™์€ ์ฐธ์กฐ๋ฅผ ์œ ์ง€ํ•˜๋„๋ก ํ•˜๋Š” ๊ฒƒ,
๊ทธ๊ฒŒ ๋ฐ”๋กœ useCallback๊ณผ useMemo๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ•ต์‹ฌ ์ด์œ ๋‹ค.

 

์šฐ๋ฆฌ ๋ชจ๋‘ ์ธ์ƒ์—์„œ ์–ด๋А ์ •๋„์˜ ์•ˆ์ •์„ฑ(stability)์€ ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.
ํ•˜์ง€๋งŒ ์ฒ˜์Œ์— ๋งํ–ˆ๋“ฏ์ด, ๊ทธ ์•ˆ์ •์„ฑ์„ ๊ตณ์ด ์ถ”๊ตฌํ•  ํ•„์š”๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ, ์ฆ‰ ‘์“ธ๋ฐ์—†๋Š” ์•ˆ์ •์„ฑ ์ถ”๊ตฌ’๋Š” ์–ธ์ œ์ผ๊นŒ?

 


 

1.  ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ์—†๋‹ค๊ณ  ํ•ด์„œ ์„ฑ๋Šฅ ํ–ฅ์ƒ์ด ์‚ฌ๋ผ์ง€๋Š” ๊ฑด ์•„๋‹ˆ๋‹ค

์•ž์„œ ์‚ดํŽด๋ณธ ์˜ˆ์‹œ์—์„œ, ์•„์ฃผ ์‚ฌ์†Œํ•œ ๋ถ€๋ถ„๋งŒ ํ•œ๋ฒˆ ๋ฐ”๊ฟ”๋ณด์ž.

 

no-memo

function Okay() {
  const value = useMemo(() => ({ hello: 'world' }), [])
  const onChange = useCallback((result) => console.log(result), [])

  return <Component value={value} onChange={onChange} />
}

 

์ฐจ์ด์ ์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋Š”๊ฐ€? ๋งž๋‹ค — ์ด์ œ value์™€ onChange๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•˜์ง€ ์•Š๋Š”๋‹ค.
๊ทธ๋ƒฅ ์ผ๋ฐ˜์ ์ธ React ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์ผ ๋ฟ์ด๋‹ค.

์ด๋Ÿฐ ์ƒํ™ฉ์€ ํŠนํžˆ ๊ฒฐ๊ตญ์—” ๊ฐ’์ด React์˜ ๊ธฐ๋ณธ(built-in) ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ๋  ๋•Œ ์ž์ฃผ ๋ฐœ์ƒํ•œ๋‹ค:

 

react-built-ins

function MyButton() {
  const onClick = useCallback(
    (event) => console.log(event.currentTarget.value),
    []
  )

  return <button onClick={onClick} />
}

 

 

์—ฌ๊ธฐ์„œ onClick์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•ด๋„ ์•„๋ฌด๋Ÿฐ ํšจ๊ณผ๊ฐ€ ์—†๋‹ค.
์™œ๋ƒํ•˜๋ฉด ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๋Š” onClick์ด ์ฐธ์กฐ๊ฐ€ ์•ˆ์ •์ ์ธ์ง€ ์—ฌ๋ถ€๋ฅผ ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

“์•„๋ฌด ํšจ๊ณผ๊ฐ€ ์—†๋‹ค(Achieves nothing)”๋ผ๊ณ  ํ‘œํ˜„ํ–ˆ์ง€๋งŒ, ์—„๋ฐ€ํžˆ ๋งํ•˜๋ฉด ์•ฝ๊ฐ„ ์ž˜๋ชป๋œ ํ‘œํ˜„์ด๋‹ค. ์‚ฌ์‹ค ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” ๋ถ„๋ช… ๋ฌด์–ธ๊ฐ€ ์ผ์ด ์ผ์–ด๋‚˜๊ณ  ์žˆ๋‹ค.

React๋Š” onClick ํ•จ์ˆ˜๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์บ์‹œ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•˜๊ณ , ์˜์กด์„ฑ์„ ์ถ”์ ํ•˜๋ฉฐ ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๋น„๊ต๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. useCallback์— ์ „๋‹ฌ๋œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋„ ๋งค ๋ Œ๋”๋งˆ๋‹ค ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์ง€๋งŒ, ์บ์‹œ๋œ ๋ฒ„์ „์ด ๋ฐ˜ํ™˜๋˜๋ฉด ์ฆ‰์‹œ ๋ฒ„๋ ค์ง„๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ, ๊ธฐ์ˆ ์ ์œผ๋กœ๋Š” ๋‚ด๋ถ€์— ์•ฝ๊ฐ„์˜ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์ƒ๊ธด๋‹ค. ํ•˜์ง€๋งŒ ๋‚˜๋Š” ์ด ์˜ค๋ฒ„ํ—ค๋“œ์— ๋„ˆ๋ฌด ์ง‘์ค‘ํ•˜๊ณ  ์‹ถ์ง„ ์•Š๋‹ค. ์™œ๋ƒํ•˜๋ฉด ์ง„์งœ ๋ฌธ์ œ๋Š” ๊ทธ๊ฒŒ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

 

์ฆ‰, ์‚ฌ์šฉ์ž ์ •์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค๋ฉด, ์ฐธ์กฐ ์•ˆ์ •์„ฑ(referential stability)์€ ์‚ฌ์‹ค ์‹ ๊ฒฝ ์“ธ ํ•„์š”๊ฐ€ ์—†๋‹ค๋Š” ๋œป์ด๋‹ค.

 

ํ•˜์ง€๋งŒ ์ž ๊น — ๋งŒ์•ฝ ๊ทธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ props๋ฅผ ๋‚ด๋ถ€์—์„œ useEffect์˜ ์˜์กด์„ฑ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ๋‹ค๋ฅธ ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ ๊ฐ’์„ ๋งŒ๋“ค์–ด ๊ทธ ๊ฐ’์„ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?


์ด ๊ฒฝ์šฐ, ์ง€๊ธˆ useMemo๋‚˜ useCallback์„ ์ œ๊ฑฐํ•˜๋ฉด , ๊ธฐ์กด ๋ฉ”๋ชจ์ด์ œ์ด์…˜์„ ์ œ๊ฑฐํ•˜๋ฉด ๋ฌด์–ธ๊ฐ€๊ฐ€ ๊นจ์งˆ ์ˆ˜๋„ ์žˆ๋‹ค.

๋ฐ”๋กœ ์ด ์ ์ด ๋‘ ๋ฒˆ์งธ ํฌ์ธํŠธ๋กœ ์ด์–ด์ง„๋‹ค.

 

2. props๋ฅผ ์˜์กด์„ฑ์œผ๋กœ ์‚ฌ์šฉํ•  ๋•Œ

์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌ๋ฐ›์€ ์›์‹œ ํƒ€์ž…์ด ์•„๋‹Œ(non-primitive) ๋น„์›์‹œ๊ฐ’ props๋ฅผ ๋‚ด๋ถ€ useEffect๋‚˜ useMemo ๋“ฑ์˜ ์˜์กด์„ฑ ๋ฐฐ์—ด์— ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š” ๊ฑด์€ ๊ฑฐ์˜ ๋Œ€๋ถ€๋ถ„ ์ ์ ˆํ•˜์ง€ ์•Š๋‹ค.


์™œ๋ƒํ•˜๋ฉด ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ๊ทธ props์˜ ์ฐธ์กฐ ์•ˆ์ •์„ฑ์— ๋Œ€ํ•ด ์–ด๋–ค ํ†ต์ œ๊ถŒ๋„ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

ํ”ํ•œ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

props-as-dependencies

function OhNo({ onChange }) {
  const handleChange = useCallback((e: React.ChangeEvent) => {
    trackAnalytics('changeEvent', e)
    onChange?.(e)
  }, [onChange])

  return <SomeMemoizedComponent onChange={handleChange} />
}

 

 

์ด useCallback์€ ์•„๋งˆ ์“ธ๋ชจ๊ฐ€ ์—†๊ฑฐ๋‚˜,
๊ธฐ๊ปํ•ด์•ผ ์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์ด ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋А๋ƒ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ๋ฟ์ด๋‹ค.

๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ, ํ˜ธ์ถœํ•˜๋Š” ์ชฝ์—์„œ ๊ทธ๋ƒฅ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๋‹ค.

 

<OhNo onChange={() => props.doSomething()} />โ€‹

 

 

์ด๊ฑด ๋ฌดํ•ดํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋‹ค. ์ „ํ˜€ ๋ฌธ์ œ๋  ๊ฒŒ ์—†๋‹ค.
์‚ฌ์‹ค ์˜คํžˆ๋ ค ์ข‹์€ ํŒจํ„ด์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์™€ ๊ด€๋ จ๋œ ๋™์ž‘์„ ํ•œ๊ณณ์— ๋ชจ์•„๋‘์–ด, ํŒŒ์ผ ์ƒ๋‹จ์œผ๋กœ ๋Œ์–ด์˜ฌ๋ ค์„œ ๊ธธ๊ฒŒ ์ด๋ฆ„ ๋ถ™์ธ handleChange ๊ฐ™์€ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“œ๋Š” ์ผ์„ ํ”ผํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๊นจ์ง„๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ ์ˆ˜ ์žˆ๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์€, ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€๋ฅผ ๊นŠ์ด ํŒŒ๊ณ  ๋“ค์–ด๊ฐ€ props๊ฐ€ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ๋ฟ์ด๋‹ค.
์†”์งํžˆ ๋งํ•ด, ๊ทธ๊ฑด ๋”์ฐํ•˜๋‹ค.

 

๋‹ค๋ฅธ ํ•ด๊ฒฐ์ฑ…์œผ๋กœ๋Š” “๋ชจ๋“  ๊ฑธ ํ•ญ์ƒ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•œ๋‹ค”๋ผ๋Š” ์ •์ฑ…์„ ์„ธ์šฐ๊ฑฐ๋‚˜,
์ฐธ์กฐ ์•ˆ์ •์„ฑ์ด ํ•„์š”ํ•œ props์—๋Š” mustBeMemoized ๊ฐ™์€ ์—„๊ฒฉํ•œ ๋„ค์ด๋ฐ ๊ทœ์น™์„ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.
ํ•˜์ง€๋งŒ ์ด ๋‘˜ ๋‹ค ์ข‹์€ ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ๋‹ค.

 

์‹ค์ œ ์‚ฌ๋ก€(Real Life Example)

์ง€๊ธˆ ๋‚˜๋Š” Sentry ์ฝ”๋“œ๋ฒ ์ด์Šค์—์„œ ์ž‘์—…ํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์ด๊ฑด ์˜คํ”ˆ์†Œ์Šค๋ผ์„œ ๐ŸŽ‰ ์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์‰ฝ๊ฒŒ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค.

๋‚ด๊ฐ€ ๋ฐœ๊ฒฌํ•œ ํ•œ ๊ฐ€์ง€ ์ƒํ™ฉ์€ useHotkeys ์ปค์Šคํ…€ ํ›…์ด๋‹ค.
ํ•ต์‹ฌ ๋ถ€๋ถ„์€ ๋Œ€๋žต ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ๋‹ค:

 

useHotkeys

export function useHotkeys(hotkeys: Hotkey[]): {
  const onKeyDown = useCallback(() => ..., [hotkeys])

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)

    return () => {
      document.removeEventListener('keydown', onKeyDown)
    }
  }, [onKeyDown])
}

 

 

์ด ์ปค์Šคํ…€ ํ›…์€ hotKeys ๋ฐฐ์—ด์„ ์ธ์ž๋กœ ๋ฐ›๊ณ ,
๊ทธ๊ฑธ ๊ธฐ๋ฐ˜์œผ๋กœ onKeyDown ํ•จ์ˆ˜๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•œ ๋’ค, ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ effect์— ๋„˜๊ธด๋‹ค.

 

ํ•จ์ˆ˜๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋Š” ์ด์œ ๋Š” ๋ช…๋ฐฑํ•˜๋‹ค: Effect๊ฐ€ ๋„ˆ๋ฌด ์ž์ฃผ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋‹ค.


ํ•˜์ง€๋งŒ ์ž…๋ ฅ๊ฐ’์ธ hotkeys๊ฐ€ ๋ฐฐ์—ด์ด๊ธฐ ๋•Œ๋ฌธ์—, ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ ์ง์ ‘ hotkeys๋ฅผ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

 

๊ทธ๋ž˜์„œ ๋‚˜๋Š” useHotkeys๊ฐ€ ์‹ค์ œ ์ฝ”๋“œ์—์„œ ์–ด๋–ป๊ฒŒ ์“ฐ์ด๊ณ  ์žˆ๋Š”์ง€ ์ „์ˆ˜ ์กฐ์‚ฌ๋ฅผ ํ•ด๋ดค๋‹ค.
๋†€๋ž๊ฒŒ๋„ ๊ฑฐ์˜ ๋Œ€๋ถ€๋ถ„(ํ•œ ๊ณณ์„ ์ œ์™ธํ•˜๊ณ )์€ ์ž…๋ ฅ๊ฐ’์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ์ด์•ผ๊ธฐ๋Š” ์—ฌ๊ธฐ์„œ ๋๋‚˜์ง€ ์•Š๋Š”๋‹ค.
๋” ๊นŠ์ด ๋“ค์–ด๊ฐ€ ๋ณด๋ฉด, ๊ฒฐ๊ตญ์—๋Š” ์—ฌ์ „ํžˆ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๊ธฐ ์‰ฌ์šด ๊ตฌ์กฐ๋ผ๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ๋ณด์ž.

 

 

paginateHotKeys

const paginateHotkeys = useMemo(() => {
  return [
    { match: 'right', callback: () => paginateItems(1) },
    { match: 'left', callback: () => paginateItems(-1) },
  ]
}, [paginateItems])

useHotkeys(paginateHotkeys)

 

 

useHotKeys๋Š” ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋œ paginateHotkeys๋ฅผ ์ „๋‹ฌํ•œ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ ์ด paginateHotkeys๋Š” paginateItems์— ์˜์กดํ•œ๋‹ค.

๊ทธ๋Ÿผ paginateItems๋Š” ์–ด๋””์„œ ์˜ฌ๊นŒ?


๋ฐ”๋กœ ๋˜ ๋‹ค๋ฅธ useCallback์ด๊ณ , ์ด ์ฝœ๋ฐฑ์€ screenshots์™€ currentAttachmentIndex์— ์˜์กดํ•œ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด screenshots๋Š” ์–ด๋””์„œ ์˜ค๋Š” ๊ฑธ๊นŒ?

 

breaking-memoization

const screenshots = attachments.filter(({ name }) =>
  name.includes('screenshot')
)

 

screenshots๋Š” ๋ฉ”๋ชจ์ด์ œ์ด์…˜๋˜์ง€ ์•Š์€ attachments.filter ํ•จ์ˆ˜์—์„œ ์ƒ์„ฑ๋œ๋‹ค.
์ด ํ•จ์ˆ˜๋Š” ๋งค๋ฒˆ ์ƒˆ๋กœ์šด ๋ฐฐ์—ด(Array)์„ ์ƒ์„ฑํ•˜๋ฏ€๋กœ, downstream์— ์žˆ๋Š” ๋ชจ๋“  ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๊นจ์ง„๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ ๋ชจ๋“  ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๋ฌด์šฉ์ง€๋ฌผ์ด ๋œ๋‹ค.


์ฆ‰, paginateItems, paginateHotkeys, onKeyDown
์ด ์„ธ ๊ฐ€์ง€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๋‹ค์‹œ ์‹คํ–‰๋˜๋ฉฐ,
๋งˆ์น˜ ์•„์˜ˆ ์ž‘์„ฑํ•˜์ง€ ์•Š์€ ๊ฒƒ๊ณผ ๋™์ผํ•œ ์ƒํ™ฉ์ด ๋œ๋‹ค! (์ด๋ ‡๊ฒŒ ๋˜๋ฉด ์šฐ๋ฆฌ๊ฐ€ ํ–ˆ๋˜ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์€ ์ „๋ถ€ ๋ฌด์šฉ์ง€๋ฌผ์ด ๋œ๋‹ค)

 

 

์ด ์˜ˆ์‹œ๊ฐ€ ๋‚ด๊ฐ€ ์™œ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ์ ์šฉ์— ๋ฐ˜๋Œ€ํ•˜๋Š” ์ž…์žฅ์ธ์ง€ ์ž˜ ๋ณด์—ฌ์คฌ์œผ๋ฉด ํ•œ๋‹ค.
๋‚ด ๊ฒฝํ—˜์ƒ, ๋ฉ”๋ชจ์ด์ œ์ด์…˜์€ ๋„ˆ๋ฌด ์ž์ฃผ ๊นจ์ง€๊ณ , ๊ทธ๋Ÿด ๊ฐ€์น˜๊ฐ€ ์—†๋‹ค.


๊ฒŒ๋‹ค๊ฐ€ ์ฝ”๋“œ ์ „์ฒด์— ์—„์ฒญ๋‚œ ์˜ค๋ฒ„ํ—ค๋“œ์™€ ๋ณต์žก์„ฑ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ ํ•ด๊ฒฐ์ฑ…์ด screenshots๊นŒ์ง€ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.


๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ์ฑ…์ž„์ด ๊ฒฐ๊ตญ ์ปดํฌ๋„ŒํŠธ์˜ prop์ธ attachments๋กœ ๋„˜์–ด๊ฐ€๊ฒŒ ๋œ๋‹ค.
์„ธ ๊ฐ€์ง€ ํ˜ธ์ถœ ์ง€์ (call-side) ๋ชจ๋‘์—์„œ ์‹ค์ œ๋กœ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ํ•„์š”ํ•œ useHotkeys์™€๋Š” ์ตœ์†Œ ๋‘ ๋‹จ๊ณ„ ์ด์ƒ ๋–จ์–ด์ ธ ์žˆ๊ฒŒ ๋œ๋‹ค.


์ด๊ฑด ์ฝ”๋“œ๋ฅผ ํƒ์ƒ‰ํ•˜๊ธฐ ์•…๋ชฝ ๊ฐ™์€ ์ƒํ™ฉ์„ ๋งŒ๋“ค๋ฉฐ, ๊ฒฐ๊ตญ ์•„๋ฌด๋„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ํ•˜๋‚˜๋ฅผ ์ œ๊ฑฐํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ๋œ๋‹ค.
์™œ๋ƒํ•˜๋ฉด ๊ทธ ๋ฉ”๋ชจ์ด์ œ์ด์…˜์ด ์‹ค์ œ๋กœ ๋ฌด์—‡์„ ํ•˜๊ณ  ์žˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์‹ค์ œ๋กœ๋Š”, ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ์ปดํŒŒ์ผ๋Ÿฌ์—๊ฒŒ ๋ชจ๋“  ๊ฑธ ๋งก๊ธฐ๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹๋‹ค.


๋ชจ๋“  ํ™˜๊ฒฝ์—์„œ ์ž˜ ์ž‘๋™ํ•œ๋‹ค๋ฉด ์ •๋ง ํ›Œ๋ฅญํ•˜๋‹ค.
ํ•˜์ง€๋งŒ ๊ทธ ์ „๊นŒ์ง€๋Š”, ์ฐธ์กฐ ์•ˆ์ •์„ฑ(referential stability) ํ•„์š”์„ฑ์˜ ํ•œ๊ณ„๋ฅผ ์šฐํšŒํ•  ํŒจํ„ด์„ ์ฐพ์•„์•ผ ํ•œ๋‹ค.

 


 

 

์ตœ์‹  Ref ํŒจํ„ด(The Latest Ref Pattern)

์ด ํŒจํ„ด์— ๋Œ€ํ•ด์„œ๋Š” ์ด์ „์— ์–ธ๊ธ‰ํ•œ ์ ์ด ์žˆ๋‹ค.
ํ•ต์‹ฌ ์•„์ด๋””์–ด๋Š”, Effect ์•ˆ์—์„œ ์ฆ‰์‹œ ์ ‘๊ทผํ•˜๊ณ  ์‹ถ์€ ๊ฐ’์„ ref ์•ˆ์— ์ €์žฅํ•ด๋‘๊ณ ,
๋˜ ๋‹ค๋ฅธ Effect๋ฅผ ํ†ตํ•ด ์˜๋„์ ์œผ๋กœ ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

 

the-latest-ref

export function useHotkeys(hotkeys: Hotkey[]): {
  const hotkeysRef = useRef(hotkeys)

  useEffect(() => {
    hotkeysRef.current = hotkeys
  })

  const onKeyDown = useCallback(() => ..., [])

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)

    return () => {
      document.removeEventListener('keydown', onKeyDown)
    }
  }, [])
}

 

 

๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด hotkeysRef๋ฅผ Effect ์•ˆ์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
์˜์กด์„ฑ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•  ํ•„์š”๋„ ์—†๊ณ , ๋‹จ์ˆœํžˆ ๋ฆฐํ„ฐ๋ฅผ ๋ฌด์‹œํ•˜๋‹ค๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” stale closure ๋ฌธ์ œ๋„ ๊ฑฑ์ •ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

์‹ค์ œ๋กœ React Query๋„ ์ด ํŒจํ„ด์„ ์‚ฌ์šฉํ•œ๋‹ค.


์˜ˆ๋ฅผ ๋“ค์–ด PersistQueryClientProvider๋‚˜ useMutationState์—์„œ ์ตœ์‹  ์˜ต์…˜(latest options)์„ ์ถ”์ ํ•  ๋•Œ ๊ทธ๋ ‡๋‹ค.
์ฆ‰, ๊ฒ€์ฆ๋œ(tried-and-true) ํŒจํ„ด์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
๋งŒ์•ฝ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์‚ฌ์šฉ์ž๊ฐ€ ์˜ต์…˜์„ ์ง์ ‘ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜๋„๋ก ๊ฐ•์ œํ•œ๋‹ค๋ฉด ์–ผ๋งˆ๋‚˜ ๊ท€์ฐฎ์„์ง€ ์ƒ์ƒํ•ด๋ณด๋ผ.

 

 

UseEffectEvent

์ข‹์€ ์†Œ์‹์ด ํ•˜๋‚˜ ๋” ์žˆ๋‹ค.
React ํŒ€์€ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ, ๋ฆฌ์•กํ‹ฐ๋ธŒ Effect ์•ˆ์—์„œ ์ตœ์‹  ๊ฐ’์„ ๋ช…๋ นํ˜•(imperative)์œผ๋กœ ์ ‘๊ทผํ•  ํ•„์š”๊ฐ€ ์žˆ์ง€๋งŒ, Effect๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์ง€ ์•Š์€ ์ƒํ™ฉ์ด ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์ธ์‹ํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ด ํŒจํ„ด์„ ์œ„ํ•œ 1๊ธ‰ ํ”„๋ฆฌ๋ฏธํ‹ฐ๋ธŒ(first-class primitive), useEffectEvent๋ฅผ ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•  ์˜ˆ์ •์ด๋‹ค.

 

์ด ํ›…์ด ๋„์ž…๋˜๋ฉด, ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ๋ณด๋‹ค ๊น”๋”ํ•˜๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ๋ฆฌํŒฉํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค:

 

the-latest-ref

export function useHotkeys(hotkeys: Hotkey[]): {
  const onKeyDown = useEffectEvent(() => ...)

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)

    return () => {
      document.removeEventListener('keydown', onKeyDown)
    }
  }, [])
}

 

 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด onKeyDown์€ ๋ฆฌ์•กํ‹ฐ๋ธŒํ•˜์ง€ ์•Š๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ hotkeys์˜ ์ตœ์‹  ๊ฐ’์„ ํ•ญ์ƒ “๋ณผ ์ˆ˜ ์žˆ๊ฒŒ” ๋˜๋ฉฐ, ๋ Œ๋” ์‚ฌ์ด์—์„œ๋„ ์ฐธ์กฐ ์•ˆ์ •์„ฑ(referential stability) ์„ ์œ ์ง€ํ•œ๋‹ค.

์ฆ‰, useCallback์ด๋‚˜ useMemo๋ฅผ ๋‹จ ํ•˜๋‚˜๋„ ์“ฐ์ง€ ์•Š๊ณ ๋„, ๋ชจ๋“  ์žฅ์ ์„ ๋™์‹œ์— ์–ป์„ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

 

 

์ •๋ฆฌ

๋‹จ์ˆœํžˆ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋‚˜ ์•ˆ์ •์„ฑ์„ ์ด์œ ๋กœ ๋ฌด์ž‘์ • useMemo/useCallback์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋Œ€๋ถ€๋ถ„ ๋ถˆํ•„์š”ํ•˜๊ฑฐ๋‚˜ ์˜คํžˆ๋ ค ๋ฌธ์ œ๋ฅผ ๋งŒ๋“ ๋‹ค.
๋Œ€์‹  Ref๋ฅผ ํ™œ์šฉํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ์šด React ํŒจํ„ด์„ ์‚ฌ์šฉํ•ด ์ตœ์‹  ๊ฐ’์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฐธ์กฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์“ฐ๋Š” ๊ฒŒ ์ข‹๋‹ค.

 

 

 

nuqs๋Š” Next.js์—์„œ URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ƒํƒœ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

์˜ค๋Š˜์€ 'nuqs'์˜ useQueryState์— ๋Œ€ํ•ด ์•Œ์•„๋ณผ ๊ฒƒ์ด๋‹ค.

 

 

๊ธฐ๋ณธ ๊ฐœ๋…

useQueryState๋Š” React์˜ useState์™€ ๋น„์Šทํ•˜๊ฒŒ ๋™์ž‘ํ•˜์ง€๋งŒ, ์ƒํƒœ๋ฅผ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€๊ฐ€ ์•„๋‹Œ URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

 

import { useQueryState } from 'nuqs'

function SearchComponent() {
  const [search, setSearch] = useQueryState('search')
  
  // URL: /?search=hello
  // search ๊ฐ’: "hello"
  
  return (
    <input 
      value={search || ''} 
      onChange={(e) => setSearch(e.target.value)}
    />
  )
}

 

์ฃผ์š” ํŠน์ง•

URL๊ณผ ์ž๋™ ๋™๊ธฐํ™”

  • ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด URL์ด ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค
  • ๋’ค๋กœ๊ฐ€๊ธฐ/์•ž์œผ๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ์ด ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค
  • URL์„ ๊ณต์œ ํ•˜๋ฉด ๊ฐ™์€ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

ํƒ€์ž… ์•ˆ์ „์„ฑ

  • ํŒŒ์„œ(parser)๋ฅผ ์‚ฌ์šฉํ•ด ํƒ€์ž…์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
import { useQueryState, parseAsInteger, parseAsBoolean } from 'nuqs'

const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
const [isActive, setIsActive] = useQueryState('active', parseAsBoolean)

 

 

๊ธฐ๋ณธ๊ฐ’ ์„ค์ •

const [sort, setSort] = useQueryState('sort', { defaultValue: 'name' })

 

 

์—ฌ๋Ÿฌ ํŒŒ๋ผ๋ฏธํ„ฐ ๋™์‹œ ๊ด€๋ฆฌ

import { useQueryStates } from 'nuqs'

const [filters, setFilters] = useQueryStates({
  search: parseAsString,
  page: parseAsInteger.withDefault(1),
  category: parseAsString
})

// ์—ฌ๋Ÿฌ ๊ฐ’์„ ํ•œ ๋ฒˆ์— ์—…๋ฐ์ดํŠธ
setFilters({ search: 'test', page: 1 })

 

 

์‹ค์ œ ์‚ฌ์šฉ ์˜ˆ์‹œ

๊ฒ€์ƒ‰๊ณผ ํ•„ํ„ฐ๋ง์ด ์žˆ๋Š” ์ƒํ’ˆ ๋ชฉ๋ก:

function ProductList() {
  const [search, setSearch] = useQueryState('search')
  const [category, setCategory] = useQueryState('category')
  const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))

  return (
    <div>
      <input 
        value={search || ''} 
        onChange={(e) => setSearch(e.target.value)}
        placeholder="๊ฒ€์ƒ‰..."
      />
      
      <select 
        value={category || ''} 
        onChange={(e) => setCategory(e.target.value)}
      >
        <option value="">์ „์ฒด</option>
        <option value="electronics">์ „์ž์ œํ’ˆ</option>
        <option value="clothing">์˜๋ฅ˜</option>
      </select>
      
      {/* URL: /?search=laptop&category=electronics&page=2 */}
    </div>
  )
}

 

 

์žฅ์ 

  • SEO ์นœํ™”์ : ํ•„ํ„ฐ๋ง๋œ ์ƒํƒœ๊ฐ€ URL์— ์žˆ์–ด ํฌ๋กค๋Ÿฌ๊ฐ€ ์ธ๋ฑ์‹ฑ ๊ฐ€๋Šฅ
  • ๊ณต์œ  ๊ฐ€๋Šฅ: URL๋งŒ์œผ๋กœ ์ •ํ™•ํ•œ ํŽ˜์ด์ง€ ์ƒํƒœ ๊ณต์œ 
  • ์ƒˆ๋กœ๊ณ ์นจ ์•ˆ์ „: ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์ƒํƒœ ์œ ์ง€
  • ๋ธŒ๋ผ์šฐ์ € ํžˆ์Šคํ† ๋ฆฌ: ๋’ค๋กœ๊ฐ€๊ธฐ/์•ž์œผ๋กœ๊ฐ€๊ธฐ ์ง€์›

Next.js์˜ App Router์™€ Pages Router ๋ชจ๋‘์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํŠนํžˆ ๊ฒ€์ƒ‰, ํ•„ํ„ฐ๋ง, ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ๋•Œ ๋งค์šฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

 

 

์˜ฌ๋ฐ”๋ฅธ ์ ‘๊ทผ: 1์ฐจ์› DP


๊ฐ ๋ฌผ๊ฑด์„ A์—๊ฒŒ ๋งก๊ธฐ๋ฉด A ํ”์ ์€ +a, B๋Š” ๊ทธ๋Œ€๋กœ

B์—๊ฒŒ ๋งก๊ธฐ๋ฉด B ํ”์ ์€ +b, A๋Š” ๊ทธ๋Œ€๋กœ

๋ชจ๋“  ๋ฌผ๊ฑด์„ ์ฒ˜๋ฆฌํ–ˆ์„ ๋•Œ A์˜ ๋ˆ„์  ํ”์  < n, B์˜ ๋ˆ„์  ํ”์  < m์„ ๋งŒ์กฑํ•˜๋ฉด์„œ A์˜ ํ”์  ํ•ฉ์„ ์ตœ์†Œํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค


function solution(info, n, m) {
  const INF = m + 1; // m ์ด์ƒ์ด๋ฉด ์ด๋ฏธ ์‹คํŒจ ์ƒํƒœ๋ผ ๊ฐ„๋‹จํžˆ ์ปท
  // dp[a] = A ํ”์ ์ด a์ผ ๋•Œ ๊ฐ€๋Šฅํ•œ B์˜ ์ตœ์†Œ ํ”์ 
  let dp = new Array(n).fill(INF);
  dp[0] = 0;

  for (const [aTrace, bTrace] of info) {
    const next = new Array(n).fill(INF);
    for (let a = 0; a < n; a++) {
      const b = dp[a];
      if (b >= m) continue; // ์ด๋ฏธ ์‹คํŒจ ์ƒํƒœ๋Š” ํ™•์žฅํ•˜์ง€ ์•Š์Œ

      // 1) ์ด ๋ฌผ๊ฑด์„ A๊ฐ€ ํ›”์น˜๋Š” ๊ฒฝ์šฐ
      const na = a + aTrace;
      if (na < n) {
        if (next[na] > b) next[na] = b;
      }

      // 2) ์ด ๋ฌผ๊ฑด์„ B๊ฐ€ ํ›”์น˜๋Š” ๊ฒฝ์šฐ
      const nb = b + bTrace;
      if (nb < m) {
        if (next[a] > nb) next[a] = nb;
      }
    }
    dp = next;
  }

  // A์˜ ๋ˆ„์  ํ”์ ์„ ์ตœ์†Œํ™”ํ•ด์„œ ์ฐพ๊ธฐ
  for (let a = 0; a < n; a++) {
    if (dp[a] < m) return a;
  }
  return -1;
}


1. ๋ฌธ์ œ ํ•ต์‹ฌ

- ๊ฐ ๋ฌผ๊ฑด์€ A๊ฐ€ ํ›”์น˜๊ฑฐ๋‚˜ B๊ฐ€ ํ›”์น˜๊ฑฐ๋‚˜ ๋‘˜ ์ค‘ ํ•œ ๋ช…๋งŒ ํ›”์น  ์ˆ˜ ์žˆ์Œ
- A, B๊ฐ€ ๋‚จ๊ธฐ๋Š” ํ”์ ์€ ๊ฐ๊ฐ ๋ˆ„์ ๋˜๊ณ ,
A์˜ ํ”์  < n, B์˜ ํ”์  < m ์กฐ๊ฑด์„ ํ•ญ์ƒ ๋งŒ์กฑํ•ด์•ผ ํ•จ
- ๋ชฉํ‘œ: ๋ชจ๋“  ๋ฌผ๊ฑด์„ ํ›”์นœ ๋’ค A์˜ ํ”์  ํ•ฉ์„ ์ตœ์†Œํ™”

์ฆ‰, โ€œ๊ฐ ๋ฌผ๊ฑด์„ ๋ˆ„๊ตฌ์—๊ฒŒ ๋งก๊ธฐ๋А๋ƒโ€๋ผ๋Š” ์„ ํƒ์˜ ์กฐํ•ฉ ๋ฌธ์ œ์ด๊ณ ,
๋ชจ๋“  ๊ฒฝ์šฐ๋ฅผ ๋‹ค ๋”ฐ์ง€๋ฉด 2^(๋ฌผ๊ฑด ๊ฐœ์ˆ˜)๋ผ์„œ ๋ถˆ๊ฐ€๋Šฅ
๊ทธ๋ž˜์„œ DP๋กœ ์ตœ์ ๊ฐ’์„ ๊ตฌํ•œ๋‹ค


2. DP ์ •์˜


์šฐ๋ฆฌ๊ฐ€ ๊ถ๊ธˆํ•œ ๊ฑด โ€œ์–ด๋–ค A์˜ ํ”์  ํ•ฉ์ด ๊ฐ€๋Šฅํ•˜๋ƒ?โ€์˜ˆ์š”.
๊ทธ๋ž˜์„œ ์ด๋ ‡๊ฒŒ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค

dp[a] = A์˜ ํ”์  ํ•ฉ์ด ์ •ํ™•ํžˆ a์ผ ๋•Œ, ๊ฐ€๋Šฅํ•œ B์˜ ์ตœ์†Œ ํ”์  ํ•ฉ

- a๋Š” 0 ~ n-1 (n ์ด์ƒ์ด๋ฉด ๊ฒฝ์ฐฐ์—๊ฒŒ ์žกํž˜)
- dp[a] ๊ฐ’์ด m ์ด์ƒ์ด๋ฉด ์˜๋ฏธ ์—†์Œ(์žกํžˆ๊ฑฐ๋‚˜ ๋ถˆ๊ฐ€๋Šฅ ์ƒํƒœ)


3. ์ดˆ๊ธฐ ์ƒํƒœ

์•„์ง ์•„๋ฌด ๋ฌผ๊ฑด๋„ ํ›”์น˜์ง€ ์•Š์•˜์„ ๋•Œ

dp[0] = 0  // A ํ”์  0, B ํ”์  0
dp[๋‹ค๋ฅธ ๊ฐ’] = ๋ถˆ๊ฐ€๋Šฅ (m+1 ๊ฐ™์€ ํฐ ๊ฐ’์œผ๋กœ ์„ค์ •)


4. ์ ํ™”์‹ (๋ฌผ๊ฑด ํ•˜๋‚˜์”ฉ ๋ฐฐ์ •)


๋ฌผ๊ฑด [aTrace, bTrace]๋ฅผ ๋งŒ๋‚ฌ์„ ๋•Œ ์„ ํƒ์€ 2๊ฐ€์ง€

1. A๊ฐ€ ํ›”์น˜๋Š” ๊ฒฝ์šฐ
โ€ข ์ƒˆ๋กœ์šด A ํ”์ : na = a + aTrace
โ€ข ์ƒˆ๋กœ์šด B ํ”์ : ๊ทธ๋Œ€๋กœ b
โ€ข ๋‹จ, na < n ์ด์–ด์•ผ ํ•จ

next[na] = min(next[na], b)


2. B๊ฐ€ ํ›”์น˜๋Š” ๊ฒฝ์šฐ
โ€ข ์ƒˆ๋กœ์šด A ํ”์ : ๊ทธ๋Œ€๋กœ a
โ€ข ์ƒˆ๋กœ์šด B ํ”์ : nb = b + bTrace
โ€ข ๋‹จ, nb < m ์ด์–ด์•ผ ํ•จ

next[a] = min(next[a], nb)



์ฆ‰, ํ•œ ๋ฌผ๊ฑด์„ A์—๊ฒŒ ๋งก๊ธฐ๊ฑฐ๋‚˜ B์—๊ฒŒ ๋งก๊ธธ ๋•Œ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ด ์ค๋‹ˆ๋‹ค

5. ์ตœ์ข… ๊ฒฐ๊ณผ


๋ชจ๋“  ๋ฌผ๊ฑด์„ ์ฒ˜๋ฆฌํ•œ ํ›„

dp[a] < m ์ธ a ์ค‘์—์„œ ๊ฐ€์žฅ ์ž‘์€ a๊ฐ€ ์ •๋‹ต.
์—†๋‹ค๋ฉด -1



๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ๊ตฌํ˜„: Web Worker์™€ WebAssembly

๋น„๋™๊ธฐ๋กœ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค? ๋งŽ์ด๋“ค async/await๋งŒ ์“ฐ๋ฉด ์ž‘์—…์ด โ€œ๋™์‹œ์—โ€ ๋ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋น„๋™๊ธฐ๋Š” ํ•˜๋‚˜์˜ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์ผ์„ ์ž˜๊ฒŒ ์ชผ๊ฐœ ์ˆœ์„œ ์—†์ด ์ฒ˜๋ฆฌํ•ด ๋ณด์ด๋Š” ๊ธฐ๋ฒ•์ด์ง€, ์‹ค์ œ๋กœ ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋ณ‘๋ ฌ๋กœ ๊ณ„์‚ฐํ•˜๋Š” ๊ฑด ์•„๋‹™๋‹ˆ๋‹ค.

UI๊ฐ€ ๋ฉˆ์ถ”์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๋ฐ๋Š” ๋น„๋™๊ธฐ๋กœ๋„ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ, ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋‚˜ ์ด๋ฏธ์ง€/์˜์ƒ ๋ณ€ํ™˜์ฒ˜๋Ÿผ CPU๋ฅผ ์žก์•„๋จน๋Š” ์ž‘์—…์€ ๊ฒฐ๊ตญ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋ฅผ ๋ฌถ์–ด๋ฒ„๋ฆฝ๋‹ˆ๋‹ค.

1. ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ๊ฐ€ ํ•„์š”ํ•œ ์ด์œ 

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ ์–ธ์–ด์ž…๋‹ˆ๋‹ค. ์ฆ‰, ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์˜ ์ž‘์—…๋งŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ณต์žกํ•œ ๊ณ„์‚ฐ, ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ, ์ด๋ฏธ์ง€ ๋ณ€ํ™˜ ๊ฐ™์€ ์ž‘์—…์€ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ ค UI๊ฐ€ ๋ฉˆ์ถœ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ๊ฐœ๋…์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ์š”.

2. Web Worker๋กœ ๋™์‹œ์„ฑ ๊ตฌํ˜„

Web Worker๋Š” ๋ณ„๋„์˜ ์Šค๋ ˆ๋“œ์—์„œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ๋ฉ”์ธ ์Šค๋ ˆ๋“œ(UI)์™€ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋ฏ€๋กœ ๋ฌด๊ฑฐ์šด ์—ฐ์‚ฐ์„ ๋งก๊ธฐ๊ธฐ ์ข‹์Šต๋‹ˆ๋‹ค.

// worker.js (๋ณ„๋„์˜ ํŒŒ์ผ)
self.onmessage = function(event) {
  let num = event.data;
  let result = 0;
  for (let i = 0; i < num; i++) {
    result += i;
  }
  self.postMessage(result);
};
// main.js
const worker = new Worker("worker.js");
worker.postMessage(1000000); // Worker์—๊ฒŒ ์ž‘์—… ์š”์ฒญ

worker.onmessage = (e) => {
  console.log("๊ฒฐ๊ณผ:", e.data);
};



Web Worker๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด UI ๋ฉˆ์ถค ์—†์ด ๋ฌด๊ฑฐ์šด ๊ณ„์‚ฐ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3. WebAssembly๋ฅผ ํ™œ์šฉํ•œ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ

WebAssembly(WASM)๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋™์ž‘ํ•˜๋Š” ์ €์ˆ˜์ค€ ์ด์ง„ ์ฝ”๋“œ ํฌ๋งท์ž…๋‹ˆ๋‹ค. ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•œ ์—ฐ์‚ฐ์„ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋Œ€์‹  C/C++/Rust ๊ฐ™์€ ์–ธ์–ด๋กœ ์ž‘์„ฑํ•œ ๋’ค, WebAssembly๋กœ ์ปดํŒŒ์ผํ•ด์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// ์˜ˆ์‹œ: C ์ฝ”๋“œ (sum.c)
int sum(int n) {
  int result = 0;
  for (int i = 0; i < n; i++) {
    result += i;
  }
  return result;
}

์ด ์ฝ”๋“œ๋ฅผ emscripten ๊ฐ™์€ ๋„๊ตฌ๋กœ WebAssembly๋กœ ์ปดํŒŒ์ผํ•˜๋ฉด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ ๊ณ ์„ฑ๋Šฅ์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4. Web Worker vs WebAssembly ๋น„๊ต

๊ตฌ๋ถ„ Web Worker WebAssembly
์ฃผ์š” ๋ชฉ์  UI์™€ ๋ถ„๋ฆฌ๋œ ๋น„๋™๊ธฐ ์‹คํ–‰ ๊ณ ์„ฑ๋Šฅ ์—ฐ์‚ฐ (C/C++/Rust ํ™œ์šฉ)
์žฅ์  JS๋งŒ์œผ๋กœ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ๊ฐ€๋Šฅ ๋„ค์ดํ‹ฐ๋ธŒ์— ๊ฐ€๊นŒ์šด ์„ฑ๋Šฅ
๋‹จ์  ์„ฑ๋Šฅ์€ JS ํ•œ๊ณ„ ๋‚ด ํ•™์Šต/๋นŒ๋“œ ๊ณผ์ •์ด ํ•„์š”

์›น ํ™˜๊ฒฝ์—์„œ๋„ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์–‘ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ„๋‹จํ•œ ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ๋Š” Web Worker, ๊ณ ์„ฑ๋Šฅ ์—ฐ์‚ฐ์€ WebAssembly๋ฅผ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ ์„ฑ๊ฒฉ์— ๋งž์ถฐ ๋‘ ๊ธฐ์ˆ ์„ ์ ์ ˆํžˆ ์กฐํ•ฉํ•˜๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์€ ์ง€๋‚œ ๋ช‡ ๋…„ ๋™์•ˆ ๋ˆˆ๋ถ€์‹œ๊ฒŒ ๋ฐœ์ „ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ณผ๊ฑฐ์—๋Š” ๋‹จ์ˆœํžˆ <script> ํƒœ๊ทธ๋กœ JS ํŒŒ์ผ์„ ๋ถˆ๋Ÿฌ์˜ค๋ฉด ๋˜์—ˆ์ง€๋งŒ, ๊ทœ๋ชจ๊ฐ€ ์ปค์ง€๊ณ  ๋ชจ๋“ˆ ๋‹จ์œ„ ๊ฐœ๋ฐœ์ด ๋ณดํŽธํ™”๋˜๋ฉด์„œ โ€œ๋ชจ๋“ˆ ๋ฒˆ๋“ค๋Ÿฌ(Module Bundler)โ€๊ฐ€ ํ•„์ˆ˜ ๋„๊ตฌ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๋Œ€ํ‘œ์ ์ธ ๋ชจ๋“ˆ ๋ฒˆ๋“ค๋Ÿฌ์ธ Webpack, Rollup.js, ๊ทธ๋ฆฌ๊ณ  ์ฐจ์„ธ๋Œ€ ๋ฒˆ๋“ค๋ง ๋„๊ตฌ๋กœ ๊ฐ๊ด‘๋ฐ›๋Š” Vite์— ๋Œ€ํ•ด ๋น„๊ตํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


1. ๋ชจ๋“ˆ ๋ฒˆ๋“ค๋Ÿฌ๋ž€

๋ชจ๋“ˆ ๋ฒˆ๋“ค๋Ÿฌ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ JavaScript, CSS, ์ด๋ฏธ์ง€ ๋“ฑ์˜ ํŒŒ์ผ์„ ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„(Dependency Graph)๋กœ ๋ถ„์„ํ•ด ํ•˜๋‚˜์˜ ํŒŒ์ผ(๋˜๋Š” ์ตœ์ ํ™”๋œ ์—ฌ๋Ÿฌ ํŒŒ์ผ)๋กœ ๋ฌถ์–ด์ฃผ๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.

๋ชฉ์ : ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํšจ์œจ์ ์œผ๋กœ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋„๋ก ์ตœ์ ํ™”๋œ ๋ฒˆ๋“ค์„ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๊ฒƒ.

์™œ ํ•„์š”ํ•œ๊ฐ€?

๐Ÿ‘€์†๋„ ๊ฐœ์„ : ํŒŒ์ผ์ด ๋งŽ์œผ๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ•˜๋‚˜์”ฉ ์š”์ฒญํ•ด์•ผ ํ•ด์„œ ๋А๋ ค์š”. ๋ฒˆ๋“ค๋Ÿฌ๊ฐ€ ํŒŒ์ผ์„ ํ•ฉ์ณ์„œ ์š”์ฒญ์„ ์ค„์—ฌ์ค๋‹ˆ๋‹ค.
๐Ÿ‘€์ตœ์ ํ™”: ์“ฐ์ด์ง€ ์•Š๋Š” ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐ(ํŠธ๋ฆฌ ์‰์ดํ‚น)ํ•˜๊ฑฐ๋‚˜, ์ฝ”๋“œ๋ฅผ ์••์ถ•ํ•ด ์šฉ๋Ÿ‰์„ ์ค„์—ฌ์ค๋‹ˆ๋‹ค.
๐Ÿ‘€๊ฐœ๋ฐœ ํŽธ์˜์„ฑ: ์ตœ์‹  ๋ฌธ๋ฒ•(ES6, TypeScript ๋“ฑ)์„ ๊ตฌ๋ฒ„์ „ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ์“ธ ์ˆ˜ ์žˆ๋„๋ก ๋ณ€ํ™˜ํ•ด ์ค๋‹ˆ๋‹ค.


2. ์ฃผ์š” ๋ชจ๋“ˆ ๋ฒˆ๋“ค๋Ÿฌ

์ด์ œ ๋งŽ์ด ์“ฐ์ด๋Š” 3๊ฐ€์ง€ ๋„๊ตฌ๋ฅผ ๋น„๊ตํ•ด๋ณผ๊ฒŒ์š”

Webpack

๊ฐ€์žฅ ์œ ๋ช…ํ•˜๊ณ  ์˜ค๋ž˜๋œ ๋ฒˆ๋“ค๋Ÿฌ
์„ค์ •์ด ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ๋ณต์žกํ•˜๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค
entry โ†’ loader โ†’ plugin โ†’ output ๊ตฌ์กฐ๋กœ ๋™์ž‘.

๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—์„œ ์•ˆ์ •์ ์ด๊ณ  ํ™•์žฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.

์˜ˆ์‹œ
React, Vue ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๊ธฐ๋ณธ ๋ฒˆ๋“ค๋Ÿฌ๋กœ ๋งŽ์ด ์“ฐ์˜€์Šต๋‹ˆ๋‹ค.
๋ณต์žกํ•œ ๋นŒ๋“œ(์ด๋ฏธ์ง€, CSS, JS, TS ๋‹ค ํ•ฉ์น˜๊ธฐ)์— ์œ ๋ฆฌ.

Rollup.js

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ œ์ž‘์— ๊ฐ•์ .
Webpack๋ณด๋‹ค ๋‹จ์ˆœํ•˜๊ณ  ๊ฐ€๋ณ์Šต๋‹ˆ๋‹ค.
ํŠธ๋ฆฌ ์‰์ดํ‚น(tree shaking) ๊ธฐ๋Šฅ์ด ๊ฐ•๋ ฅํ•ด์„œ, ์“ฐ์ง€ ์•Š๋Š” ์ฝ”๋“œ๊ฐ€ ๊ฑฐ์˜ ๋‚จ์ง€ ์•Š์•„์š”.

์˜ˆ์‹œ:
Lodash, D3.js ๊ฐ™์€ ์œ ๋ช…ํ•œ JS ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ Rollup์œผ๋กœ ๋นŒ๋“œ๋ฉ๋‹ˆ๋‹ค.
๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ œ์ž‘ํ•  ๋•Œ๋Š” Webpack๋ณด๋‹ค Rollup์ด ๋” ์„ ํ˜ธ๋ฉ๋‹ˆ๋‹ค.


Vite

์ฐจ์„ธ๋Œ€ ๋ฒˆ๋“ค๋Ÿฌ๋กœ, ์ตœ๊ทผ ๊ฐ€์žฅ ํ•ซํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๋ถ€์ ์œผ๋กœ Rollup์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์ง€๋งŒ, ๊ฐœ๋ฐœ ์„œ๋ฒ„๋Š” ESBuild(Go ์–ธ์–ด๋กœ ๋งŒ๋“  ์ดˆ๊ณ ์† ๋ฒˆ๋“ค๋Ÿฌ)๋ฅผ ์‚ฌ์šฉ


๐Ÿ”ด์žฅ์ : ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์†๋„๊ฐ€ ์—„์ฒญ ๋น ๋ฆ„ (๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ์ฆ‰์‹œ ๋ฐ˜์˜ โ†’ HMR)

๐Ÿ”ด๋‹จ์ : ์•„์ง์€ Webpack๋งŒํผ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ƒํƒœ๊ณ„๊ฐ€ ๋ฐฉ๋Œ€ํ•˜์ง€ ์•Š์Œ


์˜ˆ์‹œ:
Vue 3, React ์ตœ์‹  ํ…œํ”Œ๋ฆฟ์—์„œ ๊ธฐ๋ณธ ๋นŒ๋“œ ๋„๊ตฌ๋กœ ์ฑ„ํƒ๋จ
์Šคํƒ€ํŠธ์—…์ด๋‚˜ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์—์„œ ๋น ๋ฅด๊ฒŒ ๊ฐœ๋ฐœํ•  ๋•Œ ์ตœ์ 


ํšŒ์‚ฌ์—์„œ๋„ ๊ธฐ์กด์— React + Webpack ๊ธฐ๋ฐ˜์œผ๋กœ ์šด์˜ํ•˜๋˜ ์„œ๋น„์Šค๋ฅผ Vite๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•œ ๊ฒฝํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ๊ฐ€ ๋ฌด๊ฑฐ์›Œ์ง€๋ฉด์„œ Webpack ํ™˜๊ฒฝ์—์„œ ๋นŒ๋“œ ์†๋„๊ฐ€ ์˜ค๋ž˜๊ฑธ๋ ค ์ดˆ๊ธฐ ๊ฐœ๋ฐœ ์„œ๋ฒ„๋ฅผ ์‹œ์ž‘ํ•˜๋Š”๋ฐ์— 10๋ถ„ ์ •๋„๊ฐ€ ๊ฑธ๋ฆฌ๋Š” ์•„์ฃผ ํž˜๋“  ์ƒํ™ฉ์ด ์ƒ๊ฒจ๋ฒ„๋ฆฐ๊ฑฐ์ฃ ! HMR ์†๋„๋„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•จ๊ณผ ์ตœ์‹  ์ƒํƒœ๊ณ„์™€ ํ˜ธํ™˜์„ฑ์„ ๊ณ ๋ คํ•ด์„œ ๋…ผ์˜ ๊ฒฐ๊ณผ vite๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •์„ ํ–ˆ์Šต๋‹ˆ๋‹ค



๊ทธ ๊ฒฐ๊ณผ, ๊ฐœ๋ฐœ ์„œ๋ฒ„ ๊ตฌ๋™ ์†๋„๊ฐ€ 1๋ถ„ ๋ฏธ๋งŒ์œผ๋กœ ๋‹จ์ถ•, HMR์†๋„๋Š” ๋งํ•  ๊ฒƒ๋„ ์—†์ด ํ˜„์ €ํžˆ ๋นจ๋ผ์กŒ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก ์ ์œผ๋กœ, Vite๋Š” Webpack ๋Œ€๋น„ ํ›จ์”ฌ ๊ฐ€๋ณ๊ณ  ๋น ๋ฅธ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์„ ์ œ๊ณตํ–ˆ๊ณ , ํŒ€์˜ ์ƒ์‚ฐ์„ฑ์—๋„ ๊ธ์ •์ ์ธ ํšจ๊ณผ๋ฅผ ์คฌ์Šต๋‹ˆ๋‹ค.


https://www.felgus.dev/blog/future-spa



์ตœ๊ทผ ํ”„๋ก ํŠธ์—”๋“œ ์ƒํƒœ๊ณ„์—์„œ ๊ฐ€์žฅ ๋œจ๊ฑฐ์šด ์ฃผ์ œ ์ค‘ ํ•˜๋‚˜๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ(Server Components) ์™€ ์„œ๋ฒ„ ๋ Œ๋”๋ง์ž…๋‹ˆ๋‹ค. Next.js์™€ ๊ฐ™์€ ๋ฉ”ํƒ€ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ๊ฐ๊ด‘๋ฐ›์œผ๋ฉด์„œ โ€œSPA๋Š” ์ด์ œ ๋’ค์ณ์ง€๋Š” ๊ฒŒ ์•„๋‹๊นŒ?โ€๋ผ๋Š” ๋…ผ์˜๋„ ์ž์ฃผ ๋“ฑ์žฅํ•˜์ฃ .


ํ•˜์ง€๋งŒ ๋‹จ์ˆœํ•œ SPA(Single Page Application), ์ฆ‰ ์ •์  HTML๊ณผ JS ํŒŒ์ผ ํ•˜๋‚˜๋กœ ์‹œ์ž‘ํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๋„ ์—ฌ์ „ํžˆ ๊ฐ•๋ ฅํ•œ ๊ฒฝ์Ÿ๋ ฅ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ์˜คํžˆ๋ ค ์ƒˆ๋กœ์šด React ๊ธฐ๋Šฅ๋“ค์„ ์ ์ ˆํžˆ ํ™œ์šฉํ•˜๋ฉด ๊ธฐ์กด SPA์˜ ํ•œ๊ณ„๋ฅผ ๊ทน๋ณตํ•˜๊ณ  ํ›จ์”ฌ ๋งค๋„๋Ÿฌ์šด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜(UX)์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” SPA์—์„œ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ์‹  ์ „๋žต๋“ค์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

1. Render-on-Fetch์—์„œ Render-as-You-Fetch๋กœ


์ „ํ†ต์ ์ธ SPA๋Š” ๋ณดํ†ต ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค:

1. HTML ์š”์ฒญ
2. JS ๋‹ค์šด๋กœ๋“œ ํ›„ React ์ดˆ๊ธฐํ™”
3. ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ์‹œ์ž‘
4. ์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ fetch ํ˜ธ์ถœ
5. Suspense fallback UI ํ‘œ์‹œ
6. ์‘๋‹ต ๋„์ฐฉ ํ›„ ์‹ค์ œ UI ๋ Œ๋”๋ง

์ด ๋ฐฉ์‹์€ ๋‹จ์ˆœํ•˜์ง€๋งŒ ๋„คํŠธ์›Œํฌ ์›Œํ„ฐํด์ด ์ƒ๊ธฐ๋ฉฐ UI ํ‘œ์‹œ๊ฐ€ ์ง€์—ฐ๋ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋ฉด Render-as-You-Fetch ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ์ง์ „์— ๋ผ์šฐํŠธ ๋กœ๋”(Route Loader) ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋” ๋นจ๋ฆฌ ์š”์ฒญํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


์ฆ‰, ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ณผ์ •๊ณผ UI ๋ Œ๋”๋ง์„ ๋งž๋ฌผ๋ ค ๋™์‹œ์— ์ง„ํ–‰ํ•˜๋ฏ€๋กœ ์ดˆ๊ธฐ ๋ฐ˜์‘ ์†๋„๊ฐ€ ๊ฐœ์„ ๋ฉ๋‹ˆ๋‹ค.

โ†’ React Router v7, Tanstack Router ๊ฐ™์€ ์ตœ์‹  ๋ผ์šฐํ„ฐ๊ฐ€ ์ด ํŒจํ„ด์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

2. ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ์ •์  ํ™œ์šฉ

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋งํ•ด์•ผ ํ•  ๊ฒƒ์ฒ˜๋Ÿผ ๋“ค๋ฆฌ์ง€๋งŒ, ๊ผญ ๊ทธ๋ ‡์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค.

๋นŒ๋“œ ๋‹จ๊ณ„์—์„œ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•œ ์ •์  Server Component Payload๋ฅผ ํ™œ์šฉํ•ด SPA ์•ˆ์— ํฌํ•จ์‹œํ‚ฌ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด, ์„ธ์…˜ ์ƒํƒœ๋‚˜ ์‚ฌ์šฉ์ž ์ž…๋ ฅ๊ณผ ๋ฌด๊ด€ํ•œ ์ปดํฌ๋„ŒํŠธ๋Š” ๋นŒ๋“œ ์‹œ์ ์— ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด ๋‘๊ณ , SPA ์‹คํ–‰ ์‹œ ํ•„์š”ํ•œ ์ˆœ๊ฐ„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.
โ†’ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์„œ๋ฒ„ ํ™˜๊ฒฝ์ด ์—†์–ด๋„ SPA ์„ฑ๋Šฅ๊ณผ ์ดˆ๊ธฐ ๋กœ๋”ฉ ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3. Concurrent Features: ๋” ๋งค๋„๋Ÿฌ์šด ์ธํ„ฐ๋ž™์…˜


React 18์—์„œ ๋„์ž…๋œ useTransition, useDeferredValue ๊ฐ™์€ ๋™์‹œ์„ฑ ๊ธฐ๋Šฅ์€ ๋‹จ์ˆœํžˆ โ€œ๋Œ€ํ˜• ์„œ๋น„์Šค ์ „์šฉโ€์ด ์•„๋‹™๋‹ˆ๋‹ค.
ํ”„๋ก ํŠธ์—”๋“œ์—์„œ์˜ ๊ทœ๋ชจ(scale) ๋Š” ์‚ฌ์šฉ์ž ์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ผ โ€œํŽ˜์ด์ง€, ์ƒํ˜ธ์ž‘์šฉ, ์ •๋ณด ํ๋ฆ„์˜ ๋ณต์žก๋„โ€๋กœ ์ •์˜๋ฉ๋‹ˆ๋‹ค.
SPA์—์„œ๋„ ์‚ฌ์šฉ์ž๋Š” ์•„๋ฌด ๋•Œ๋‚˜ ๋‹ค์–‘ํ•œ ์ธํ„ฐ๋ž™์…˜์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ์—…๋ฐ์ดํŠธ์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ช…ํ™•ํžˆ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด UX๊ฐ€ ์‰ฝ๊ฒŒ ๋Š๊ธฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋™์‹œ์„ฑ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋ฉด React๊ฐ€ ๋†’์€ ์šฐ์„ ์ˆœ์œ„ ์ž‘์—…๋ถ€ํ„ฐ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์ง€์‹œํ•  ์ˆ˜ ์žˆ๊ณ , ๊ทธ ๊ฒฐ๊ณผ ๋” ๋ถ€๋“œ๋Ÿฌ์šด ํ™”๋ฉด ์ „ํ™˜๊ณผ ๋ฐ˜์‘์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4. Preload๋ฅผ ํ†ตํ•œ ์„ ์ œ์  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜

์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…๊ณผ ์ง€์—ฐ ๋กœ๋”ฉ์€ ์ด๋ฏธ ํ”ํ•œ ๊ธฐ๋ฒ•์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„œ ๋” ๋‚˜์•„๊ฐ€ SPA ๋ผ์šฐํ„ฐ์˜ Link ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์„ฑ๋Šฅ์„ ๋˜ ํ•œ ๋‹จ๊ณ„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ๋งํฌ ์œ„์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ ค๋†“์€ ์ˆœ๊ฐ„, ๋ผ์šฐํ„ฐ๊ฐ€ ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ์™€ ์ฝ”๋“œ ์Šคํ”Œ๋ฆฟ๋œ JS๋ฅผ ๋ฏธ๋ฆฌ ์š”์ฒญํ•˜๋„๋ก ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์‹ค์ œ ํŽ˜์ด์ง€ ์ „ํ™˜ ์‹œ ์ด๋ฏธ ์ค€๋น„๋œ ์ฝ˜ํ…์ธ ๊ฐ€ ์ฆ‰์‹œ ํ‘œ์‹œ๋˜์–ด, ๋งˆ์น˜ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์ฒ˜๋Ÿผ ๋ฐ˜์‘ํ•˜๋Š” UX๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

5. React Labs์˜ ์‹ ๊ธฐ๋Šฅ: ViewTransition & Activity

React ํŒ€์ด ๋ฐœํ‘œํ•œ ์‹คํ—˜์  ๊ธฐ๋Šฅ ์ค‘ ๋‘ ๊ฐ€์ง€๊ฐ€ SPA UX์— ํŠนํžˆ ์ฃผ๋ชฉํ•  ๋งŒํ•ฉ๋‹ˆ๋‹ค:
โ€ข ViewTransitionโ€จ์ƒˆ๋กœ์šด ์›น ํ‘œ์ค€์œผ๋กœ, ํŽ˜์ด์ง€ ์ „ํ™˜ ์‹œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋” ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก React์™€ ๋ธŒ๋ผ์šฐ์ € API ๊ฐ„์˜ ๊ฐ„๊ทน์„ ๋ฉ”์›Œ์ค๋‹ˆ๋‹ค.โ€จโ†’ ๊ฒฐ๊ณผ์ ์œผ๋กœ ํ™”๋ฉด ์ „ํ™˜ ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ž์—ฐ์Šค๋Ÿฌ์›Œ์ง‘๋‹ˆ๋‹ค.
โ€ข Activity (๊ตฌ Offscreen)โ€จ๋ Œ๋”๋ง๊ณผ ์ปค๋ฐ‹ ๊ณผ์ • ์‚ฌ์ด์— โ€œ๋ฏธ๋ฆฌ UI ์Šค๋ƒ…์ƒท์„ ์ค€๋น„ํ•ด๋‘๋Š”โ€ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.
โ€จ์ฆ‰, ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด์ด์ง€ ์•Š๋Š” ์ƒํƒœ๋กœ ๋ Œ๋”๋งํ•ด๋‘” ๋’ค, ํ•„์š”ํ•  ๋•Œ ์ฆ‰์‹œ ํ™”๋ฉด์— ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.โ€จโ†’ ์ „ํ™˜ ์‹œ์ ์—์„œ ๊ธฐ๋‹ค๋ฆผ ์—†์ด ๊ณง๋ฐ”๋กœ ์ƒˆ๋กœ์šด ํ™”๋ฉด์„ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ธฐ๋Šฅ์„ Preload ์ „๋žต๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด, ํŽ˜์ด์ง€ ์ „ํ™˜ ์†๋„ ๋ฐ ์—ฐ์†์„ฑ์ด ๋Œ€ํญ ๊ฐœ์„ ๋ฉ๋‹ˆ๋‹ค.

๊ฒฐ๋ก : SPA๋Š” ๋” ๋ถ€๋“œ๋Ÿฝ๊ฒŒ, ๋” ๋น ๋ฅด๊ฒŒ
SPA๊ฐ€ ์„œ๋ฒ„ ๋ Œ๋”๋ง ํ”„๋ ˆ์ž„์›Œํฌ์— ๋ฐ€๋ ค ์‚ฌ๋ผ์งˆ ๊ฑฐ๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ธฐ ์‰ฝ์ง€๋งŒ, ์˜คํžˆ๋ ค ๋ฐ˜๋Œ€์ž…๋‹ˆ๋‹ค.
โ€ข Render-as-you-fetch ํŒจํ„ด
โ€ข Server Component์˜ ์ •์  ํ™œ์šฉ
โ€ข Concurrent Features๋กœ ๋ถ€๋“œ๋Ÿฌ์šด ์ธํ„ฐ๋ž™์…˜
โ€ข Preload & Activity & ViewTransition์œผ๋กœ ๋„ค์ดํ‹ฐ๋ธŒ ๊ฐ™์€ ์ „ํ™˜ ๊ฒฝํ—˜

์ด ์ „๋žต๋“ค์„ ๊ฒฐํ•ฉํ•˜๋ฉด SPA๋Š” ์ง€๊ธˆ๋ณด๋‹ค ํ›จ์”ฌ ๊ฐ•๋ ฅํ•˜๊ณ  ์„ธ๋ จ๋œ UX๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ ์ฆ‰, SPA์˜ ๋ฏธ๋ž˜๋Š” ์„ฑ๋Šฅ๊ณผ UX๋ฅผ ๋ชจ๋‘ ๊ฐ–์ถ˜ โ€œ๋” ๋˜‘๋˜‘ํ•œ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ Œ๋”๋งโ€์ž…๋‹ˆ๋‹ค.

+ Recent posts