{"id":920,"date":"2026-05-20T09:57:06","date_gmt":"2026-05-20T01:57:06","guid":{"rendered":"https:\/\/junai.ai\/blog\/nodejs-mini-project-26\/"},"modified":"2026-05-20T09:57:06","modified_gmt":"2026-05-20T01:57:06","slug":"nodejs-mini-project-26","status":"publish","type":"post","link":"https:\/\/junai.ai\/blog\/nodejs-mini-project-26\/","title":{"rendered":"\uc2e4\uc804 \ubbf8\ub2c8 \ud504\ub85c\uc81d\ud2b8 \u2014 Todo API \uc11c\ubc84 \uc644\uacb0"},"content":{"rendered":"\n<!-- WordPress REST API \ubc1c\ud589\uc6a9 HTML (\uc790\ub3d9 \uc0dd\uc131) -->\n<!-- WP-FEATURED-MEDIA-ID: 866 -->\n<div style=\"max-width:800px;margin:0 auto;\">\n<style>\n:root {--color-primary:#059669;--color-accent:#10b981;--color-bg:#fafbfc;--color-bg-card:#fff;--color-text:#1a202c;--color-text-muted:#64748b;--hero-start:#064e3b;--hero-end:#059669;}\n*{box-sizing:border-box;}\n.container{max-width:760px;margin:0 auto;padding:0 24px 80px;}\n.hero{background:linear-gradient(135deg,var(--hero-start) 0%,var(--hero-end) 100%);color:#fff;padding:80px 24px 60px;text-align:center;}\n.hero .eyebrow{display:inline-block;font-size:14px;color:#6ee7b7;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:14px;}\n.hero h1{font-size:36px;margin:0 0 16px;line-height:1.3;font-weight:800;}\n.hero p{color:#d1fae5;font-size:18px;max-width:640px;margin:0 auto;line-height:1.6;}\n.hero img{width:100%;max-width:640px;height:auto;margin:32px auto 0;border-radius:10px;display:block;}\narticle{padding-top:48px;}\narticle h2{font-size:26px;margin:56px 0 20px;padding-left:14px;border-left:5px solid var(--color-accent);line-height:1.4;}\narticle h3{font-size:19px;margin:32px 0 12px;color:var(--color-primary);}\narticle p{margin:16px 0;}\narticle strong{color:var(--color-primary);font-weight:700;}\narticle code{background:#d1fae5;padding:2px 8px;border-radius:4px;font-family:'SF Mono',Menlo,Consolas,monospace;font-size:14px;color:#065f46;}\n.databox{background:#d1fae5;border-left:4px solid var(--color-accent);padding:16px 20px;margin:24px 0;border-radius:0 8px 8px 0;font-size:15.5px;}\n.databox strong{color:var(--color-primary);}\n.warnbox{background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);padding:16px 20px;margin:24px 0;border-radius:8px;font-size:15.5px;}\n.tablewrap{overflow-x:auto;-webkit-overflow-scrolling:touch;margin:22px 0;}\ntable{width:100%;border-collapse:collapse;font-size:15px;background:var(--color-bg-card);}\nth,td{padding:11px 12px;text-align:left;border-bottom:1px solid #e2e8f0;vertical-align:top;}\nth{background:#f1f5f9;font-weight:700;color:#0f172a;}\ntd:first-child,th:first-child{font-weight:700;}\n@media (max-width:560px){.tablewrap table,.tablewrap thead,.tablewrap tbody,.tablewrap tr,.tablewrap th,.tablewrap td{display:block;width:auto;}.tablewrap thead{display:none;}.tablewrap tr{margin:0 0 14px;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;}.tablewrap td{border:none;border-bottom:1px solid #f1f5f9;padding:9px 14px;}.tablewrap td:first-child{background:#f1f5f9;font-weight:800;font-size:15.5px;}.tablewrap td:last-child{border-bottom:none;}.tablewrap td[data-label]::before{content:attr(data-label) \" \u2014 \";font-weight:700;color:var(--color-primary);}}\n.code-block{background:#0f172a;color:#e2e8f0;padding:16px 20px;border-radius:8px;font-family:'SF Mono',Menlo,Consolas,monospace;font-size:14px;line-height:1.6;margin:20px 0;overflow-x:auto;white-space:pre;}\n.cta{background:linear-gradient(135deg,#059669 0%,#10b981 100%);color:#fff;padding:28px 24px;border-radius:12px;margin:48px 0 0;text-align:center;}\n.cta h3{color:#fff;margin:0 0 8px;font-size:20px;}\n.cta p{color:#d1fae5;margin:0;font-size:15.5px;}\n.footer-nav{margin-top:32px;padding-top:20px;border-top:1px solid #e2e8f0;font-size:14px;color:var(--color-text-muted);}\n.footer-nav a{color:var(--color-primary);text-decoration:none;}\n@media (max-width:480px){.hero h1{font-size:26px;}.hero p{font-size:16px;}article h2{font-size:21px;}article h3{font-size:17px;}body{font-size:16px;}}\n<\/style>\n<section class=\"hero\">\n  <span class=\"eyebrow\">Node.js \uad50\uc7ac \u00b7 26\ud3b8 \u00b7 \uc644\uacb0<\/span>\n  <h1>\uc2e4\uc804 \ubbf8\ub2c8 \ud504\ub85c\uc81d\ud2b8 \u2014 Todo API \uc11c\ubc84 \uc644\uacb0<\/h1>\n  <p>25\ud3b8\uc758 \ubaa8\ub4e0 \uac1c\ub150\uc774 \ud55c \ud504\ub85c\uc81d\ud2b8\uc5d0\uc11c. \uc9c4\uc9dc \ud68c\uc0ac\uc5d0\uc11c \uc4f8 \ub9cc\ud55c \ubc31\uc5d4\ub4dc \ud55c \uc138\ud2b8.<\/p>\n  <img decoding=\"async\" src=\"https:\/\/junai.ai\/blog\/wp-content\/uploads\/2026\/05\/hero-5-97.jpg\" alt=\"Todo API \uc11c\ubc84 \uc644\uc131 \ucee8\uc149 \uc77c\ub7ec\uc2a4\ud2b8\">\n<\/section>\n\n<div class=\"container\">\n<article>\n\n<p>25\ud3b8\uc744 \ub05d\ub0b8 \uc0ac\ub78c\uc758 \ub2e4\uc74c \u2014 &#8220;\uc774 \uc870\uac01\ub4e4\uc744 \uc5b4\ub5bb\uac8c \ud55c \uc11c\ube44\uc2a4\ub85c \ud569\uce58\uc9c0?&#8221;. \ub9c8\uc9c0\ub9c9 \ucc55\ud130\ub294 \uadf8 \ub2f5. <strong>\uc778\uc99d \uc788\ub294 Todo API \uc11c\ubc84<\/strong>\ub97c \ucc98\uc74c\ubd80\ud130 \ub05d\uae4c\uc9c0 \ub9cc\ub4e4\uba74\uc11c Node \uc2dc\ub9ac\uc988 \ubaa8\ub4e0 \uac1c\ub150\uc774 \ud55c \uc790\ub9ac\uc5d0\uc11c \ub9cc\ub09c\ub2e4.<\/p>\n\n<p>\uc644\uc131 \uacb0\uacfc \u2014 \uc0ac\uc6a9\uc790\uac00 \ud68c\uc6d0\uac00\uc785\u00b7\ub85c\uadf8\uc778\ud558\uace0, \ubcf8\uc778 todo \ub97c \uc0dd\uc131\u00b7\uc870\ud68c\u00b7\uc218\uc815\u00b7\uc0ad\uc81c\ud560 \uc218 \uc788\uace0, JWT \ub85c \uc778\uc99d\ud558\uba70, Docker \ucee8\ud14c\uc774\ub108\ub85c \ubc30\ud3ec\ub41c \uc9c4\uc9dc API \uc11c\ubc84. \ud504\ub860\ud2b8\uc5d4\ub4dc\ub294 Next.js 26\ud3b8\uc758 \ube14\ub85c\uadf8\uac00 \uadf8\ub300\ub85c \uc5f0\uacb0 \uac00\ub2a5.<\/p>\n\n<h2>1. \uc2a4\ud399\uacfc \uae30\uc220 \ub9e4\ud551<\/h2>\n\n<div class=\"tablewrap\">\n<table>\n<thead><tr><th>\uae30\ub2a5<\/th><th>\uae30\uc220<\/th><th>\ucc38\uc870 \ucc55\ud130<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>\ud68c\uc6d0\uac00\uc785\u00b7\ub85c\uadf8\uc778<\/td><td data-label=\"\uae30\uc220\">bcrypt + jsonwebtoken<\/td><td data-label=\"\ucc38\uc870\">19<\/td><\/tr>\n<tr><td>CRUD \ub77c\uc6b0\ud2b8<\/td><td data-label=\"\uae30\uc220\">Express + Router<\/td><td data-label=\"\ucc38\uc870\">13\u00b714<\/td><\/tr>\n<tr><td>\uc785\ub825 \uac80\uc99d<\/td><td data-label=\"\uae30\uc220\">Zod<\/td><td data-label=\"\ucc38\uc870\">15<\/td><\/tr>\n<tr><td>DB<\/td><td data-label=\"\uae30\uc220\">node-postgres (pg)<\/td><td data-label=\"\ucc38\uc870\">18<\/td><\/tr>\n<tr><td>\uc778\uc99d \uac00\ub4dc<\/td><td data-label=\"\uae30\uc220\">requireAuth \ubbf8\ub4e4\uc6e8\uc5b4<\/td><td data-label=\"\ucc38\uc870\">14\u00b719<\/td><\/tr>\n<tr><td>\uc5d0\ub7ec \ucc98\ub9ac<\/td><td data-label=\"\uae30\uc220\">AppError + \ubbf8\ub4e4\uc6e8\uc5b4<\/td><td data-label=\"\ucc38\uc870\">16<\/td><\/tr>\n<tr><td>\ub85c\uae45<\/td><td data-label=\"\uae30\uc220\">winston\u00b7morgan\u00b7\uc694\uccad ID<\/td><td data-label=\"\ucc38\uc870\">21<\/td><\/tr>\n<tr><td>\ubcf4\uc548<\/td><td data-label=\"\uae30\uc220\">helmet\u00b7cors\u00b7rate-limit<\/td><td data-label=\"\ucc38\uc870\">24<\/td><\/tr>\n<tr><td>\ud14c\uc2a4\ud2b8<\/td><td data-label=\"\uae30\uc220\">Jest + supertest<\/td><td data-label=\"\ucc38\uc870\">22<\/td><\/tr>\n<tr><td>\ubc30\ud3ec<\/td><td data-label=\"\uae30\uc220\">Docker + docker-compose<\/td><td data-label=\"\ucc38\uc870\">25<\/td><\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n\n<h2>2. \ub514\ub809\ud1a0\ub9ac \uad6c\uc870<\/h2>\n\n<div class=\"code-block\">todo-api\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 server.js              \u2190 \uc5d4\ud2b8\ub9ac, graceful shutdown\n\u2502   \u251c\u2500\u2500 app.js                 \u2190 createApp() \u2014 \ud14c\uc2a4\ud2b8\uc640 \uacf5\uc720\n\u2502   \u251c\u2500\u2500 env.js                 \u2190 Zod \ud658\uacbd\ubcc0\uc218 \uac80\uc99d\n\u2502   \u251c\u2500\u2500 db.js                  \u2190 pg Pool\n\u2502   \u251c\u2500\u2500 logger.js              \u2190 winston\n\u2502   \u251c\u2500\u2500 security.js            \u2190 helmet+cors+rate-limit \ubb36\uc74c\n\u2502   \u251c\u2500\u2500 errors.js              \u2190 AppError\u00b7NotFound\u00b7Validation\n\u2502   \u251c\u2500\u2500 middleware\/\n\u2502   \u2502   \u251c\u2500\u2500 requestId.js\n\u2502   \u2502   \u251c\u2500\u2500 requireAuth.js\n\u2502   \u2502   \u2514\u2500\u2500 errorHandler.js\n\u2502   \u2514\u2500\u2500 routes\/\n\u2502       \u251c\u2500\u2500 auth.js            \u2190 \/auth\/signup, \/auth\/login\n\u2502       \u2514\u2500\u2500 todos.js           \u2190 \/todos CRUD\n\u251c\u2500\u2500 tests\/\n\u2502   \u251c\u2500\u2500 auth.test.js\n\u2502   \u2514\u2500\u2500 todos.test.js\n\u251c\u2500\u2500 Dockerfile\n\u251c\u2500\u2500 docker-compose.yml\n\u251c\u2500\u2500 .env.example\n\u251c\u2500\u2500 .env.local                 \u2190 .gitignore\n\u251c\u2500\u2500 package.json\n\u2514\u2500\u2500 README.md<\/div>\n\n<p>\ud55c \ud3f4\ub354 \uc548\uc5d0 \ubaa8\ub4e0 \uac8c \uc788\ub418 \ucc45\uc784 \ubcc4\ub85c \ud30c\uc77c \ubd84\ub9ac. Express \ud328\ud134\uc758 \ud45c\uc900 \uad6c\uc870.<\/p>\n\n<h2>3. \ud575\uc2ec \ud30c\uc77c \ubbf8\ub9ac\ubcf4\uae30<\/h2>\n\n<h3>src\/app.js \u2014 \ubaa8\ub4e0 \ubbf8\ub4e4\uc6e8\uc5b4 + \ub77c\uc6b0\ud2b8 \uc870\ub9bd<\/h3>\n\n<div class=\"code-block\">import express from &#8216;express&#8217;;\nimport { applySecurity } from &#8216;.\/security.js&#8217;;\nimport { requestId } from &#8216;.\/middleware\/requestId.js&#8217;;\nimport { errorHandler } from &#8216;.\/middleware\/errorHandler.js&#8217;;\nimport authRouter from &#8216;.\/routes\/auth.js&#8217;;\nimport todosRouter from &#8216;.\/routes\/todos.js&#8217;;\nimport morgan from &#8216;morgan&#8217;;\nimport { logger } from &#8216;.\/logger.js&#8217;;\n\nexport function createApp() {\n  const app = express();\n\n  applySecurity(app);                    \/\/ helmet\u00b7cors\u00b7rate-limit (24)\n  app.use(express.json({ limit: &#8217;10kb&#8217; }));\n  app.use(requestId);                    \/\/ 21\n  app.use(morgan(&#8216;combined&#8217;, { stream: { write: m =&gt; logger.http(m.trim()) } }));\n\n  app.get(&#8216;\/health&#8217;, (req, res) =&gt; res.json({ ok: true }));\n  app.use(&#8216;\/auth&#8217;, authRouter);\n  app.use(&#8216;\/todos&#8217;, todosRouter);\n\n  app.use(errorHandler);                 \/\/ 16\n  return app;\n}<\/div>\n\n<h3>src\/routes\/todos.js \u2014 CRUD \ud55c \ubb36\uc74c<\/h3>\n\n<div class=\"code-block\">import { Router } from &#8216;express&#8217;;\nimport { z } from &#8216;zod&#8217;;\nimport { pool } from &#8216;..\/db.js&#8217;;\nimport { requireAuth } from &#8216;..\/middleware\/requireAuth.js&#8217;;\nimport { NotFoundError } from &#8216;..\/errors.js&#8217;;\n\nconst router = Router();\nrouter.use(requireAuth);                 \/\/ \ubaa8\ub4e0 \ub77c\uc6b0\ud2b8 \uc778\uc99d\n\nconst TodoSchema = z.object({\n  title: z.string().min(1).max(200),\n  done: z.boolean().optional(),\n});\n\nrouter.get(&#8216;\/&#8217;, async (req, res) =&gt; {\n  const { rows } = await pool.query(\n    &#8216;SELECT id, title, done, created_at FROM todos WHERE user_id = $1 ORDER BY created_at DESC&#8217;,\n    [req.user.id]\n  );\n  res.json({ data: rows });\n});\n\nrouter.post(&#8216;\/&#8217;, async (req, res) =&gt; {\n  const data = TodoSchema.parse(req.body);\n  const { rows: [todo] } = await pool.query(\n    &#8216;INSERT INTO todos (user_id, title) VALUES ($1, $2) RETURNING *&#8217;,\n    [req.user.id, data.title]\n  );\n  res.status(201).json(todo);\n});\n\nrouter.patch(&#8216;\/:id&#8217;, async (req, res) =&gt; {\n  const data = TodoSchema.partial().parse(req.body);\n  const { rows: [todo] } = await pool.query(\n    &#8216;UPDATE todos SET title = COALESCE($1,title), done = COALESCE($2,done), updated_at = NOW() WHERE id = $3 AND user_id = $4 RETURNING *&#8217;,\n    [data.title, data.done, req.params.id, req.user.id]\n  );\n  if (!todo) throw new NotFoundError(&#8216;todo&#8217;);\n  res.json(todo);\n});\n\nrouter.delete(&#8216;\/:id&#8217;, async (req, res) =&gt; {\n  const { rowCount } = await pool.query(\n    &#8216;DELETE FROM todos WHERE id = $1 AND user_id = $2&#8217;,\n    [req.params.id, req.user.id]\n  );\n  if (rowCount === 0) throw new NotFoundError(&#8216;todo&#8217;);\n  res.status(204).end();\n});\n\nexport default router;<\/div>\n\n<p>\ud55c \ud30c\uc77c\uc5d0 \u2014 15\ud3b8 REST \uaddc\uc57d, 14\ud3b8 \ubbf8\ub4e4\uc6e8\uc5b4, 16\ud3b8 \uc5d0\ub7ec, 18\ud3b8 pg \ubc14\uc778\ub529, 19\ud3b8 \uc778\uc99d\uc774 \ub2e4 \ubcf4\uc778\ub2e4.<\/p>\n\n<h3>tests\/todos.test.js \u2014 supertest \ud1b5\ud569 \ud14c\uc2a4\ud2b8<\/h3>\n\n<div class=\"code-block\">import { describe, it, expect, beforeEach } from &#8216;@jest\/globals&#8217;;\nimport request from &#8216;supertest&#8217;;\nimport { createApp } from &#8216;..\/src\/app.js&#8217;;\nimport { signAccessToken } from &#8216;..\/src\/auth\/tokens.js&#8217;;\n\nconst app = createApp();\nconst token = signAccessToken(&#8216;test-user-id&#8217;);\nconst auth = `Bearer ${token}`;\n\ndescribe(&#8216;POST \/todos&#8217;, () =&gt; {\n  it(&#8216;201 with valid input&#8217;, async () =&gt; {\n    const res = await request(app)\n      .post(&#8216;\/todos&#8217;)\n      .set(&#8216;Authorization&#8217;, auth)\n      .send({ title: &#8216;\uccad\uc18c&#8217; });\n    expect(res.status).toBe(201);\n    expect(res.body.title).toBe(&#8216;\uccad\uc18c&#8217;);\n  });\n\n  it(&#8216;400 when title empty&#8217;, async () =&gt; {\n    const res = await request(app)\n      .post(&#8216;\/todos&#8217;)\n      .set(&#8216;Authorization&#8217;, auth)\n      .send({ title: &#8221; });\n    expect(res.status).toBe(400);\n  });\n\n  it(&#8216;401 when no token&#8217;, async () =&gt; {\n    const res = await request(app).post(&#8216;\/todos&#8217;).send({ title: &#8216;x&#8217; });\n    expect(res.status).toBe(401);\n  });\n});<\/div>\n\n<p>22\ud3b8 \ud328\ud134 \uadf8\ub300\ub85c. \ub9e4 PR \ub9c8\ub2e4 \uc790\ub3d9 \ud68c\uadc0 \uac80\uc0ac.<\/p>\n\n<h3>docker-compose.yml \u2014 \ud480\uc2a4\ud0dd \ud55c \uc904<\/h3>\n\n<div class=\"code-block\">services:\n  api:\n    build: .\n    ports: [&#8216;3000:3000&#8217;]\n    env_file: .env.local\n    depends_on: [db]\n    restart: unless-stopped\n\n  db:\n    image: postgres:17-alpine\n    environment:\n      POSTGRES_PASSWORD: ${DB_PASSWORD}\n      POSTGRES_DB: todoapi\n    volumes:\n      &#8211; dbdata:\/var\/lib\/postgresql\/data\n    ports: [&#8216;5432:5432&#8217;]\n\nvolumes:\n  dbdata:<\/div>\n\n<div class=\"code-block\">$ docker compose up -d\n$ curl -X POST http:\/\/localhost:3000\/auth\/signup \\\n    -H &#8220;Content-Type: application\/json&#8221; \\\n    -d &#8216;{&#8220;email&#8221;:&#8221;a@b.c&#8221;,&#8221;password&#8221;:&#8221;password123&#8243;}&#8217;<\/div>\n\n<p>\ud55c \uba85\ub839\uc73c\ub85c API + DB \uac00 \uac19\uc774 \ub77c\uc774\ube0c. 25\ud3b8 docker-compose \uc758 \ub9c8\uc9c0\ub9c9 \uc801\uc6a9.<\/p>\n\n<h2>4. \ub2e4\uc74c \ub2e8\uacc4 \u2014 \uc5b4\ub514\ub85c \uac08\uc9c0<\/h2>\n\n<p>\uc774 Todo API \uc704\uc5d0 \uc5b9\uc744 \ub9cc\ud55c \uac83\ub4e4 \u2014 \ud559\uc2b5 \uc5ec\uc815\uc758 \uc5f0\ub8cc.<\/p>\n\n<ul>\n<li><strong>\ud504\ub860\ud2b8\uc5d4\ub4dc \uc5f0\uacb0<\/strong> \u2014 Next.js 26\ud3b8\uc758 \ube14\ub85c\uadf8\uc5d0\uc11c \uc774 API \ud638\ucd9c. CORS\u00b7\uc778\uc99d \ud750\ub984 \uc9c4\uc9dc \uacbd\ud5d8.<\/li>\n<li><strong>Redis \uce90\uc2dc<\/strong> \u2014 \uc790\uc8fc \uc870\ud68c\ub418\ub294 todo \ubaa9\ub85d\uc744 Redis \uc5d0. \uc751\ub2f5 \uc18d\ub3c4 5\ubc30.<\/li>\n<li><strong>BullMQ \ud050<\/strong> \u2014 \ud68c\uc6d0\uac00\uc785 \ud658\uc601 \uba54\uc77c\uc744 \ud050\ub85c \uc704\uc784. 23\ud3b8 worker_threads \uc758 \uc9c4\ud654\ud310.<\/li>\n<li><strong>OpenAPI\/Swagger<\/strong> \u2014 API \ubb38\uc11c \uc790\ub3d9 \uc0dd\uc131. \ubaa8\ubc14\uc77c\u00b7\uc678\ubd80 \ud300\uacfc \ud611\uc5c5 \uc2dc \ud544\uc218.<\/li>\n<li><strong>Kubernetes<\/strong> \u2014 25\ud3b8 PM2\/Docker \uc758 \ub2e4\uc74c. \ub300\uaddc\ubaa8 \uc6b4\uc601\uc758 \ud45c\uc900.<\/li>\n<\/ul>\n\n<div class=\"databox\">\n<strong>26\ud3b8\uc744 \ub05d\ub0b8 \ub2f9\uc2e0<\/strong> \u2014 Node \uc758 \ub2e8\uc77c \uc2a4\ub808\ub4dc \ubaa8\ub378, Express \ubbf8\ub4e4\uc6e8\uc5b4 \ud328\ud134, REST API \uc124\uacc4, PostgreSQL CRUD, JWT \uc778\uc99d, \ubcf4\uc548 4\uc885, \ud14c\uc2a4\ud2b8\u00b7\ub85c\uae45, Docker \ubc30\ud3ec\uae4c\uc9c0 \u2014 \ud68c\uc0ac \uc2e0\uc785~\uc8fc\ub2c8\uc5b4 \ubc31\uc5d4\ub4dc \ud45c\uc900 \uc5ed\ub7c9 \uc644\ube44. 1\ub144\ucc28 \ubc31\uc5d4\ub4dc \uba74\uc811\uc5d0\uc11c \ub9c9\ud798 \uc5c6\uc774 \ub2f5\ud560 \uc815\ub3c4. \ub2e4\uc74c\uc740 <strong>\uc2e4\uc81c \uc11c\ube44\uc2a4\ub97c \uc6b4\uc601<\/strong>\ud558\uba74\uc11c \ubd80\uc871\ud568\uc744 \uba54\uc6b0\ub294 \ub2e8\uacc4.\n<\/div>\n\n<h3>\ub9c8\uce58\uba70<\/h3>\n\n<p>52\ud3b8 (Next.js 26 + Node.js 26) \ud480\uc2a4\ud0dd \uc2dc\ub9ac\uc988\uac00 \uc5ec\uae30\uc11c \ub9c8\ubb34\ub9ac. \ub05d\uae4c\uc9c0 \ub530\ub77c\uc628 \ubaa8\ub450\uc5d0\uac8c \uc9c4\uc2ec\uc73c\ub85c \ubc15\uc218. <strong>\ucc45\uc73c\ub85c \uc548 \uac00\ub294 \uae38\uc740 \ucf54\ub4dc\ub85c\ub9cc \uc775\ud600\uc9c4\ub2e4<\/strong>. \uc624\ub298 GitHub \uc5d0 Todo API \ud55c \uc800\uc7a5\uc18c \ub9cc\ub4e4\uace0 <code>docker compose up<\/code> \ubd80\ud130 \ub204\ub974\uae38.<\/p>\n\n<div class=\"cta\">\n<h3>Node.js + Next.js \uc2dc\ub9ac\uc988 52\ud3b8 \uc644\uacb0 \ud83c\udf89<\/h3>\n<p>\ud480\uc2a4\ud0dd \ud55c \ubb36\uc74c. \ubcf8\uc778 \uc0ac\uc774\ub4dc \ud504\ub85c\uc81d\ud2b8 \uc2dc\uc791 \uc2dc\uc810. \ud654\uc774\ud305.<\/p>\n<\/div>\n\n<div class=\"footer-nav\">\n\uc2dc\ub9ac\uc988 \u00b7 <a href=\"https:\/\/junai.ai\/blog\/category\/nodejs\/\">\uc27d\uac8c \ubc30\uc6b0\ub294 Node.js (\uc644\uacb0)<\/a> \u00b7 \uc774\uc804: <a href=\"https:\/\/junai.ai\/blog\/nodejs-deploy-pm2-docker-25\/\">Ch.25 PM2\u00b7Docker<\/a>\n<\/div>\n\n<\/article>\n<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Node \uc2dc\ub9ac\uc988 \ub9c8\uc9c0\ub9c9 \u2014 Todo API \uc11c\ubc84. Express\u00b7Postgres\u00b7JWT\u00b7Docker \ud1b5\ud569. \uad50\uc7ac 26\ud3b8(\uc644\uacb0).<\/p>\n","protected":false},"author":1,"featured_media":866,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[24],"tags":[],"class_list":["post-920","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-nodejs"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts\/920","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/comments?post=920"}],"version-history":[{"count":0,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/posts\/920\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/media\/866"}],"wp:attachment":[{"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/media?parent=920"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/categories?post=920"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/junai.ai\/blog\/wp-json\/wp\/v2\/tags?post=920"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}