QueryPie Community Edition is live ๐ŸŽ‰ Get it now for free Download today!

๋ฌด๋ฃŒ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ
๋ธ”๋กœ๊ทธ

Next.js Server Action๊ณผ ํ”„๋ก ํŠธ์—”๋“œ ๋ณด์•ˆ

Teddy Kim

Teddy Kim

Software Engineer

Next.js Server Action๊ณผ ํ”„๋ก ํŠธ์—”๋“œ ๋ณด์•ˆ

Next.js Server Action์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๊ณผ ์‚ฌ์šฉ๋ฒ•

Next.js์˜ ์„œ๋ฒ„ ์•ก์…˜(Server Action)์€ Next.js 13.4 ๋ฒ„์ „์—์„œ App Router์™€ ํ•จ๊ป˜ ์ •์‹์œผ๋กœ ๋„์ž…๋œ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜(Server-side function)๋ฅผ React ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ ์†์‰ฝ๊ฒŒ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค. use server ์ง€์‹œ์–ด๊ฐ€ ์žˆ๋Š” ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ฐ„ํŽธํ•˜๊ฒŒ ์„œ๋ฒ„ ๋กœ์ง์„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์„œ๋ฒ„ ์•ก์…˜์„ ํ™œ์šฉํ•˜๋ฉด ๊ธฐ์กด์ฒ˜๋Ÿผ ๋ณ„๋„์˜ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž๋Š” ์„œ๋ฒ„ ๋กœ์ง์˜ ์—”๋“œํฌ์ธํŠธ, ์ธํ„ฐํŽ˜์ด์Šค ๋“ฑ์„ ์‹ ๊ฒฝ ์“ธ ํ•„์š” ์—†์ด, ๋งˆ์น˜ ๋กœ์ปฌ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์„œ๋ฒ„ ๋กœ์ง์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์•ก์…˜์ด ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„ ํ†ต์‹ ์„ ์ถ”์ƒํ™”ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์„œ๋ฒ„ ์•ก์…˜์„ ์ •์˜ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ๋Š” React ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— ์ •์˜ํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๋ณ„๋„์˜ ํŒŒ์ผ์— ๋…๋ฆฝ์ ์œผ๋กœ ์ •์˜ํ•˜๋Š” ๋ฐฉ์‹์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ๋ณ„๋„์˜ ํŒŒ์ผ์— ์ •์˜ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

'use server';

export default async function createAction({ name, age, email }: { name: string; age: number; email: string; }) {
    await connection.query(
        'INSERT INTO users (name, age, email) VALUES (?, ?, ?)',
        [name, age, email]
    );
}
'use client';

import createAction from './create-action.ts';

export default function CreateForm() {
    return (
        <form action={createAction}>
            <input type="text" name="name" />
            <input type="number" name="age" />
            <input type="email" name="email" />
        </form>
    )
}

์„œ๋ฒ„ ์•ก์…˜์˜ ๋‚ด๋ถ€ ๋™์ž‘ ๋ฐฉ์‹

์œ„์—์„œ ์˜ˆ๋กœ ๋“  CreateForm ์ปดํฌ๋„ŒํŠธ๋Š” ๋นŒ๋“œ ์‹œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค. (์ฐธ๊ณ : ์ด๋Š” ์ดํ•ด๋ฅผ ๋•๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ์ด๋ฉฐ, ์‹ค์ œ ๋นŒ๋“œ๋œ ์ฝ”๋“œ์™€๋Š” ์ฐจ์ด๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.)

'use client';

import { startTransition } from 'react'
import { callServer } from 'next/client/app-call-server';

// ์„œ๋ฒ„ ์•ก์…˜ ID๋กœ ๋ณ€ํ™˜๋จ
const createAction = '$$action_1234567890';

export default function CreateForm() {
  return (
    <form 
      onSubmit={async (e) => {
        e.preventDefault();
        const formData = new FormData(e.currentTarget);
        const data = {
          name: formData.get('name'),
          age: Number(formData.get('age')),
          email: formData.get('email')
        };
        
        // startTransition์œผ๋กœ UI ๋ธ”๋กœํ‚น ๋ฐฉ์ง€
        startTransition(() => {
          callServer(createAction, [data]);
        });
      }}
    >
      <input type="text" name="name" />
      <input type="number" name="age" />
      <input type="email" name="email" />
    </form>
  )
}

์ฆ‰, ์„œ๋ฒ„ ์•ก์…˜ ํ•จ์ˆ˜๋ฅผ ๋กœ์ปฌ ํ•จ์ˆ˜์ฒ˜๋Ÿผ importํ•ด์„œ ์‚ฌ์šฉํ•˜๋˜ ๋ถ€๋ถ„์ด ์‚ฌ๋ผ์ง€๊ณ , ๋Œ€์‹  onSubmit ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ formData๊ฐ€ ์ฒ˜๋ฆฌ๋˜๊ณ  Next.js์—์„œ ์ œ๊ณตํ•˜๋Š” callServer ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

callServer ํ•จ์ˆ˜๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ fetch API๋ฅผ ์‚ฌ์šฉํ•ด HTTP ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญ ๋ฉ”์„œ๋“œ๋Š” POST๋กœ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉฐ, ํ—ค๋”์—๋Š” ์ƒ์„ฑ๋œ ์„œ๋ฒ„ ์•ก์…˜ ID ๋“ฑ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„ ์ธก์—์„œ ์–ด๋–ค ์•ก์…˜์ด ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€๋ฅผ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Next.js์˜ Repository์—์„œ app-call-server.ts ํŒŒ์ผ์„ ์ฐธ๊ณ ํ•˜๋ฉด ์ด๋Ÿฌํ•œ ๋™์ž‘ ๋ฐฉ์‹์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„œ๋ฒ„ ์‚ฌ์ด๋“œ์—์„œ๋Š” callServer๋กœ๋ถ€ํ„ฐ ์š”์ฒญ์„ ๋ฐ›์•„ action-handler.ts ํŒŒ์ผ์ด ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ด ํ•ธ๋“ค๋Ÿฌ๋Š” CSRF ๋ณดํ˜ธ ๋“ฑ์˜ ๊ฒ€์ฆ ๊ณผ์ •์„ ๊ฑฐ์นœ ํ›„, ์š”์ฒญ ํ—ค๋”๋ฅผ ๋ถ„์„ํ•˜์—ฌ ์ ์ ˆํ•œ ์•ก์…˜์„ ์‹๋ณ„ํ•˜๊ณ  ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•œ ๋’ค ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ๋ฆ„์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์„œ๋ฒ„ ์•ก์…˜์ด ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์— ๊ฐ€์ ธ์˜จ ๋ณ€ํ™”

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

๊ธฐ์กด์—๋Š” REST API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋งŒ๋“ค์–ด ํด๋ผ์ด์–ธํŠธ๊ฐ€ HTTP ์š”์ฒญ์„ ํ†ตํ•ด ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์ด ์ผ๋ฐ˜์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์„œ๋ฒ„ ์•ก์…˜์€ ๋กœ์ปฌ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋“ฏ ์ง๊ด€์ ์ด๊ณ  ๊ฐ„๋‹จํ•œ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด ํŽธ์˜์„ฑ์ด ๋›ฐ์–ด๋‚ฉ๋‹ˆ๋‹ค.

์˜ˆ์ „์—๋Š” "Full Stack"์ด "Poor Stack"์ด๋ผ๋ฉฐ ์กฐ๋กฑ๋ฐ›๊ธฐ๋„ ํ–ˆ์ง€๋งŒ, AI ์‹œ๋Œ€์— ์ ‘์–ด๋“ค๋ฉด์„œ ๊ฐœ๋ฐœ์ž ํ•œ ์‚ฌ๋žŒ์ด ํ•  ์ˆ˜ ์žˆ๋Š” ์ผ์ด ํฌ๊ฒŒ ๋Š˜์–ด๋‚˜ Full Stack์˜ ์œ„์ƒ์ด ๋†’์•„์กŒ์Šต๋‹ˆ๋‹ค. ์ด์ œ๋Š” ์„œ๋ฒ„ ์•ก์…˜๊ณผ ๊ฐ™์€ ๊ธฐ์ˆ  ๋•๋ถ„์— ๋”์šฑ ์‰ฝ๊ณ  ํšจ์œจ์ ์œผ๋กœ ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ด ๊ฐ€๋Šฅํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

ํŽธ๋ฆฌํ•˜์ง€๋งŒ ๋†“์ณ์„œ๋Š” ์•ˆ ๋˜๋Š” ์„œ๋ฒ„ ์•ก์…˜์˜ ๋ณด์•ˆ ๋ฌธ์ œ

Next.js์˜ ์„œ๋ฒ„ ์•ก์…˜์€ ๋งˆ์น˜ ๋งˆ๋ฒ•์ฒ˜๋Ÿผ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฐ๊ฒฐ๋œ ๋“ฏํ•œ ๋А๋‚Œ์„ ์ค๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด ๋งˆ๋ฒ• ๊ฐ™์€ ํŽธ์˜์„ฑ ๋’ค์—๋Š” ๋ฐ˜๋“œ์‹œ ๋ณด์•ˆ์ด๋ผ๋Š” ๋ฌธ์ œ๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ๋Š” HTTP ์š”์ฒญ๊ณผ ์‘๋‹ต์ด ์กด์žฌํ•˜๋ฉฐ, ์ด ๊ณผ์ •์—์„œ ๋‹ค์–‘ํ•œ ๋ณด์•ˆ์  ๊ณ ๋ ค ์‚ฌํ•ญ๋“ค์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

Next.js ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋„ ์„œ๋ฒ„ ์•ก์…˜์˜ ๋ณด์•ˆ์— ๋Œ€ํ•œ ์ฃผ์˜๋ฅผ ๋‹น๋ถ€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ ์„ ๋†“์นœ ๊ฐœ๋ฐœ์ž๋Š” ์˜๋„ํ•˜์ง€ ์•Š๊ฒŒ ์ค‘์š”ํ•œ ๋กœ์ง์„ ๋ฌด๋ฐฉ๋น„๋กœ ๋…ธ์ถœ์‹œํ‚ฌ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋Š” ๋งค์šฐ ์น˜๋ช…์ ์ธ ๋ณด์•ˆ ์ด์Šˆ๋กœ ์ด์–ด์งˆ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.(์•ˆํƒ€๊น๊ฒŒ๋„ Security ์„น์…˜์€ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ ์„น์…˜์ž…๋‹ˆ๋‹ค.)

By default, when a Server Action is created and exported, it creates a public HTTP endpoint and should be treated with the same security assumptions and authorization checks. This means, even if a Server Action or utility function is not imported elsewhere in your code, it's still publicly accessible.

๋ฌธ์„œ ์ดˆ๋ฐ˜ ์„œ๋ฒ„ ์•ก์…˜์˜ ๋งˆ๋ฒ•์— ํ™€๋ ค ๋งˆ์ง€๋ง‰ Security ์„น์…˜๊นŒ์ง€ ๋ณด์ง€ ๋ชปํ•œ ์•ˆํƒ€๊นŒ์šด ๊ฐœ๋ฐœ์ž๋“ค์€ ์•„๋ฌด๋Ÿฐ ์ž ๊ธˆ์žฅ์น˜๋ฅผ ๋‹ฌ์ง€ ์•Š์€ ์„œ๋ฒ„ ํ•จ์ˆ˜๋ฅผ ์˜จ์„ธ์ƒ์— ๊ณต๊ฐœํ•ด๋ฒ„๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ธ์ฆ, ์ธ๊ฐ€๊ฐ€ ํ•„์š”ํ•œ๋ฐ ๊ฒ€์ฆ๊ณผ์ •์„ ๋‘์ง€ ์•Š์•˜๋‹ค๋ฉด ๋” ์•„์ฐ”ํ•ฉ๋‹ˆ๋‹ค.

Next.js ์„œ๋ฒ„ ์•ก์…˜์ด ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ์ ์ธ ๋ณด์•ˆ๊ณผ ๊ฐœ๋ฐœ์ž์˜ ์—ญํ• 

๊ทธ๋ ‡๋‹ค๊ณ  ์•„์ฃผ ๋ฌด๋ฐฉ๋น„ ์ƒํƒœ๋Š” ์•„๋‹™๋‹ˆ๋‹ค. ๊ณต์‹ ๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด Next.js๋Š” ์„œ๋ฒ„ ์•ก์…˜๊ณผ ๊ด€๋ จํ•˜์—ฌ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋ณธ์ ์ธ ๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

  • Secure Action IDs: ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ๊ฐ„ ํ†ต์‹ ์˜ ์œ ํšจ์„ฑ์„ ๋‚ด๋ถ€์ ์œผ๋กœ ์ƒ์„ฑ๋œ ID๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
  • CSRF(Cross-Site Request Forgery) ๋ฐฉ์ง€: ์„œ๋ฒ„ ์•ก์…˜ ํ˜ธ์ถœ ์‹œ POST ์š”์ฒญ๋งŒ ํ—ˆ์šฉํ•˜๊ณ  ๋™์ผ ์ถœ์ฒ˜ ์š”์ฒญ๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ํด๋กœ์ € ๋ณ€์ˆ˜ ๋ณดํ˜ธ: ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌ๋˜๋Š” ์ƒํƒœ๊ฐ’์„ ์•”ํ˜ธํ™” ๋ฐ ์„œ๋ช…ํ•˜์—ฌ ๋ณดํ˜ธํ•ฉ๋‹ˆ๋‹ค.
  • ์ฝ”๋“œ ๊ฒฉ๋ฆฌ: ์„œ๋ฒ„ ์ „์šฉ ์ฝ”๋“œ๋Š” ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ณดํ˜ธ: ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฏผ๊ฐํ•œ ์ƒ์„ธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋…ธ์ถœํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ๋ฏธ์‚ฌ์šฉ ์•ก์…˜ ์ œ๊ฑฐ: ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์„œ๋ฒ„ ์•ก์…˜์„ ์ž๋™์œผ๋กœ ์ œ๊ฑฐํ•˜์—ฌ ๊ณต๊ฒฉ ํ‘œ๋ฉด ์ตœ์†Œํ™”ํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฐ˜๋“œ์‹œ ์‹ ๊ฒฝ ์จ์•ผ ํ•  ๋ถ€๋ถ„๋„ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋Ÿฐํƒ€์ž„ ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ(Validation): ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋Š” ์ปดํŒŒ์ผ ์‹œ์  ํƒ€์ž… ํ™•์ธ๋งŒ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ, ๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ์ด ํ•„์ˆ˜๋‹ค. Zod์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด ์ฒ ์ €ํžˆ ๊ฒ€์ฆํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ธ์ฆ๊ณผ ์ธ๊ฐ€: ์•ก์…˜ ๋‚ด์—์„œ ์‚ฌ์šฉ์ž ๊ถŒํ•œ์„ ๋งค๋ฒˆ ์žฌํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • SSRF(Server Side Request Forgery) ๋ฐฉ์ง€: ์‚ฌ์šฉ์ž ์ž…๋ ฅ URL ๋“ฑ์„ ์‹ ๋ขฐํ•˜์ง€ ๋ง๊ณ , ๋ฐ˜๋“œ์‹œ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ ‘๊ทผ๋งŒ ํ—ˆ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์„œ๋ฒ„ ๋กœ์ง์ด๋ฉด ํ•„์ˆ˜์ ์œผ๋กœ ๊ณ ๋ คํ•ด์•ผ ํ•  ์š”์†Œ๋“ค
    • ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ณดํ˜ธ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ •๋ณด ๋“ฑ ์ž์‚ฐ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•  ๋–„ .env ํŒŒ์ผ์— ์ €์žฅํ•˜์—ฌ ์„ค์ •๊ฐ’ ๋…ธ์ถœ์„ ๋ฐฉ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • Injection ๋ฐฉ์ง€:
      • SQL Injection: SQL Injection์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ORM(Object-Relational Mapping) ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      • XSS ๋ฐฉ์ง€: sanitize-html ๋“ฑ์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž…๋ ฅ๊ฐ’์„ ๊ฒ€์ฆํ•˜๊ณ , ์ž ์žฌ์ ์ธ Cross-Site Scripting ๊ณต๊ฒฉ์— ๋Œ€๋น„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • ๋ณด์•ˆ ์—…๋ฐ์ดํŠธ ๋ฐ ํŒจ์น˜ ๊ด€๋ฆฌ: ์‚ฌ์šฉ ์ค‘์ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํ”„๋ ˆ์ž„์›Œํฌ์— ๋Œ€ํ•œ ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ์ ๊ฒ€๊ณผ ์—…๋ฐ์ดํŠธ๋ฅผ ํ†ตํ•ด ์ทจ์•ฝ์ ์„ ์‚ฌ์ „์— ๋ฐฉ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • ๋กœ๊ทธ์™€ ๋ชจ๋‹ˆํ„ฐ๋ง: ์ค‘์š”ํ•œ ์„œ๋ฒ„ ์ด๋ฒคํŠธ์™€ ์˜ค๋ฅ˜์— ๋Œ€ํ•œ ๋กœ๊ทธ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ์„ ํ†ตํ•ด ์‹ค์‹œ๊ฐ„ ๋ณด์•ˆ ์œ„ํ˜‘์„ ํƒ์ง€ํ•˜๋„๋ก ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋Ÿฐํƒ€์ž„ ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ ๋ฐ ์ธ์ฆ๊ณผ ์ธ๊ฐ€๋Š” next-safe-action, zsa ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ์„ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ถ”์ƒํ™”๋œ API๋ฅผ ์„ ์–ธ์ ์œผ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ณ , ์ œ๊ณต๋˜๋Š” ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ†ตํ•ด ์ธ์ฆ ๋ฐ ์ธ๊ฐ€๋ฅผ ์†์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๋ฟ ์•„๋‹ˆ๋ผ ํด๋ผ์ด์–ธํŠธ ์ธก ์„œ๋ฒ„ ์•ก์…˜ ๊ด€๋ จ Hooks๋ฅผ ์ œ๊ณตํ•ด ๋”์šฑ ํŽธ๋ฆฌํ•˜๊ฒŒ ํ™œ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๋”์šฑ ์ค‘์š”ํ•ด์ง€๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๋ณด์•ˆ

๊ฐœ์ธ์ ์œผ๋กœ ํ”„๋ก ํŠธ์—”๋“œ ์˜์—ญ์—์„œ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๊ธฐ๋Šฅ์ด ๋‹ค์‹œ ๋ถ€์ƒํ•˜๋Š” ํ๋ฆ„์„ ๊ธ์ •์ ์œผ๋กœ ๋ณด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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

ํ•˜์ง€๋งŒ ์›น์€ ๋ชจ๋ฐ”์ผ ์•ฑ๊ณผ ๊ทผ๋ณธ์ ์œผ๋กœ ๋‹ค๋ฅด๋ฉฐ, ์„œ๋ฒ„์™€ ๊ธด๋ฐ€ํžˆ ์—ฐ๋™๋  ๋•Œ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ๋ฐœํœ˜ํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ค‘์‹ฌ SPA์˜ ํ•œ๊ณ„๋ฅผ ๊ทน๋ณตํ•˜๊ณ ์ž SEO์— ์œ ๋ฆฌํ•œ Next.js์™€ ๊ฐ™์€ SSR(Server Side Rendering)์„ ์ง€์›ํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ธ๊ธฐ๋ฅผ ๋Œ๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ๋Š” SSR์„ ๋„˜์–ด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋“ฑ์žฅํ•˜๋Š” ๋“ฑ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋„ ์„œ๋ฒ„ ๊ธฐ๋Šฅ์„ ๋‹ค์‹œ ์ ๊ทน์ ์œผ๋กœ ํ™œ์šฉํ•˜๋Š” ํ๋ฆ„์ด ๋‚˜ํƒ€๋‚˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

Next.js์˜ App Router์™€ ์„œ๋ฒ„ ์•ก์…˜์€ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ UI๋ฟ ์•„๋‹ˆ๋ผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์™€ ์„œ๋ฒ„ ๋กœ์ง๊นŒ์ง€ ํญ๋„“๊ฒŒ ๋‹ด๋‹นํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ฐ˜์„ ๋งŒ๋“ค์–ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ์›ฌ๋งŒํผ ๋ณต์žกํ•œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์•„๋‹Œ ์ด์ƒ DB๊ฐ€ ์กด์žฌํ•˜๋”๋ผ๋„ ์ด์ œ ๋…๋ฆฝ์ ์ธ ๋ฐฑ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์—†์ด ๊ฐœ๋ฐœ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ํšจ์œจ์ ์ธ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜์ง€๋งŒ, ๋™์‹œ์— ๋” ๋งŽ์€ ๋ณต์žก์„ฑ๊ณผ ๋ณด์•ˆ์— ๋Œ€ํ•œ ์ฑ…์ž„์ด ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

Next.js Server Action๊ณผ ํ”„๋ก ํŠธ์—”๋“œ ๋ณด์•ˆ

AI ์‹œ๋Œ€์˜ ๋ณด์•ˆ๊ณผ Next.js

AI๊ฐ€ ์ฃผ๋ชฉ๋ฐ›๋Š” ์‹œ๋Œ€๊ฐ€ ์˜ค๋ฉด์„œ ์ฃผ์š” ํ”„๋ ˆ์ž„์›Œํฌ์˜ ์•ˆ์ •์„ฑ๊ณผ ๋ณด์•ˆ์€ ๋”์šฑ ์ค‘์š”ํ•ด์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. AI ์—ญ์‹œ ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋„๊ตฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘๋™ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ํฌ๋ฏ€๋กœ, ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๊ณ ์ˆ˜์ค€์˜ ์ถ”์ƒํ™”์— ๋”ฐ๋ฅธ ๋ณด์•ˆ์  ์‹ค์ˆ˜๋ฅผ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋Š” AI๋ฅผ ํ†ตํ•ด ๋” ํฐ ์œ„ํ—˜์„ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•ž์œผ๋กœ์˜ ์›น ํ™˜๊ฒฝ์€ AI ๋“ฑ ๋‹ค์–‘ํ•œ ๊ธฐ์ˆ ๊ณผ์˜ ์œตํ•ฉ์œผ๋กœ ๋ณต์žก์„ฑ์ด ์ฆ๊ฐ€ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ Next.js์™€ ๊ฐ™์€ ๋„๋ฆฌ ์‚ฌ์šฉํ•˜๋Š” ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์€ ๋ณด์•ˆ๊ณผ ์•ˆ์ •์„ฑ ๋ฉด์—์„œ ๊ณ„์† ๋ฐœ์ „ํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋ฌด์—‡๋ณด๋‹ค AI ๊ธฐ์ˆ ๊ณผ์˜ ์œตํ•ฉ์ด ์‹ฌํ™”๋ ์ˆ˜๋ก ๊ฐœ๋ฐœ์ž ์Šค์Šค๋กœ์˜ ๋ณด์•ˆ ์ง€์‹๊ณผ ๊ฐœ๋…์ด ๋งค์šฐ ์ค‘์š”ํ•˜๋ฉฐ, ๋Š˜ ์‹ ๊ฒฝ ์จ์•ผ ํ•œ๋‹ค๋Š” ์ ์„ ์žŠ์ง€ ๋ง์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Next.js Server Action๊ณผ ํ”„๋ก ํŠธ์—”๋“œ ๋ณด์•ˆ | QueryPie