[๐ค] Next.js Server Actions ์ค์ : ์๋ฌ ์ฒ๋ฆฌ, ์ ํจ์ฑ ๊ฒ์ฌ, ๋๊ด์ UI ์ ๋ฐ์ดํธ
Next.js Server Actions๋ฅผ ์ค๋ฌด์ ์ ์ฉํ ๋ ๋ง์ฃผํ๋ ์๋ฌ ์ฒ๋ฆฌ, ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ, ๊ทธ๋ฆฌ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๋ ๋๊ด์ UI ์ ๋ฐ์ดํธ ๊ธฐ๋ฒ์ ์์ธํ ์ฝ๋ ์์์ ํจ๊ป ์์๋ณด์ธ์.
์ ๋ณด๐ค ์ด ํฌ์คํ ์ Gemini 2.5 Flash AI๊ฐ ์์ฑํ์ด์.
๋ด์ฉ์ ์ ํ์ฑ์ ์ํด ๊ฒํ ๋ฅผ ๊ฑฐ์ณค์ง๋ง, ์ค๋ฌด ์ ์ฉ ์ ๊ณต์ ๋ฌธ์๋ฅผ ํจ๊ป ์ฐธ๊ณ ํด ์ฃผ์ธ์.
์ ์ฉํ ํNext.js Server Actions๋ฅผ ํ์ฉํด ์์ ํ๊ณ ์ฌ์ฉ์ ์นํ์ ์ธ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์๋ฌ ์ฒ๋ฆฌ, ์ ํจ์ฑ ๊ฒ์ฌ, ๋๊ด์ UI ์ ๋ฐ์ดํธ ๊ธฐ๋ฒ์ ํตํด ์์ธํ ์๋ ค๋๋ ค์.
์๋
ํ์ธ์, ๋ธ๋ฃจ์์! ๐ 10๋
๋๊ฒ ํ์คํ ๊ฐ๋ฐ์๋ก ์ผํ๋ฉด์ ์๋ง์ ๊ธฐ์ ๋ณํ๋ฅผ ๊ฒฝํํด ์๋๋ฐ์, ์ต๊ทผ ํ๋ฐํธ์๋์ ๋ฐฑ์๋์ ๊ฒฝ๊ณ๋ฅผ ํ๋ฌด๋ Next.js Server Actions์ ๋ฑ์ฅ์ ์ ๋ง ํฅ๋ฏธ๋ก์ด ๋ณํ๋ผ๊ณ ์๊ฐํด์.
Server Actions๋ ๋จ์ํ ์๋ฒ ์ฝ๋๋ฅผ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ํธ์ถํ๋ ๊ฒ์ ๋์ด, ์ฌ์ฉ์ ๊ฒฝํ(UX)๊ณผ ๊ฐ๋ฐ์ ๊ฒฝํ(DX)์ ํ์ ํ ์ ์ฌ๋ ฅ์ ๊ฐ์ง๊ณ ์์ด์.
ํ์ง๋ง ์ค๋ฌด์ ์ ์ฉํ๋ ค๋ฉด ๋จ์ํ ํธ์ถํ๋ ๊ฒ์ ๋์ด, ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ, ์๋ฌ ์ฒ๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ์ฌ์ฉ์์๊ฒ ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ ๋๊ด์ UI ์
๋ฐ์ดํธ์ ๊ฐ์ ๊ณ ๊ธ ๊ธฐ๋ฒ๋ค์ ์ ์ดํดํ๊ณ ์ ์ฉํด์ผ ํด์.
์ด ๊ธ์์๋ Server Actions๋ฅผ ๋์ฑ ๊ฒฌ๊ณ ํ๊ณ ์ฌ์ฉ์ ์นํ์ ์ผ๋ก ๋ง๋๋ ์ค์ ๊ธฐ๋ฒ๋ค์ ์ด์ค๊ธ ๊ฐ๋ฐ์๋ถ๋ค์ ๋๋์ด์ ๋ง์ถฐ ์์ธํ ์๋ ค๋๋ฆด๊ฒ์.
๐ค Server Actions, ์ ์ค์ํ ๊น์?
0๏ธโฃ ํด๋ผ์ด์ธํธ-์๋ฒ ๊ฒฝ๊ณ๋ฅผ ํ๋ฌด๋ ์๋ก์ด ํจ๋ฌ๋ค์
๊ธฐ์กด ์น ๊ฐ๋ฐ์์๋ ํด๋ผ์ด์ธํธ(๋ธ๋ผ์ฐ์ )์์ ์๋ฒ์ ๋ฐ์ดํฐ๋ฅผ ์กฐ์ํ๊ฑฐ๋ ํน์ ๋ก์ง์ ์คํํ๋ ค๋ฉด REST API๋ GraphQL ๊ฐ์ ๋ณ๋์ API ์๋ํฌ์ธํธ๋ฅผ ๋ง๋ค๊ณ , ํด๋ผ์ด์ธํธ์์ fetch ๋ฑ์ ๋น๋๊ธฐ ์์ฒญ์ ๋ณด๋ด์ผ ํ์ด์.
์ด ๊ณผ์ ์์ API ๋ผ์ฐํธ ์ ์, ๋ฐ์ดํฐ ์ง๋ ฌํ/์ญ์ง๋ ฌํ, ์ํ ๊ด๋ฆฌ ๋ฑ ์ฌ๋ฌ ๋ณต์กํ ๋จ๊ณ๋ฅผ ๊ฑฐ์ณ์ผ ํ์ฃ .
ํ์ง๋ง Next.js Server Actions๋ ์ด๋ฌํ ๊ฒฝ๊ณ๋ฅผ ๋ชจํธํ๊ฒ ๋ง๋ค์ด์. ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ ์๋ฒ ์ปดํฌ๋ํธ์์ ์ง์ ์๋ฒ ์ฝ๋๋ฅผ ํธ์ถํ ์ ์๊ฒ ๋์ด, ๋ง์น ๋ก์ปฌ ํจ์๋ฅผ ํธ์ถํ๋ฏ์ด ์๋ฒ ๋ก์ง์ ์คํํ ์ ์๊ฒ ๋์์ด์.
์ด๋ ๊ฐ๋ฐ ๋ณต์ก์ฑ์ ํฌ๊ฒ ์ค์ด๊ณ , ๋ ๋น ๋ฅด๊ณ ํจ์จ์ ์ธ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ํ๋ฆ์ ๊ฐ๋ฅํ๊ฒ ํด์.
1๏ธโฃ ๊ธฐ์กด API ๋ผ์ฐํธ ๋ฐฉ์๊ณผ์ ์ฐจ์ด์
Server Actions๋ Next.js App Router์์ ์ ๊ณตํ๋ API ๋ผ์ฐํธ(Route Handlers)์ ์ ์ฌํ ๊ธฐ๋ฅ์ ์ํํ์ง๋ง, ๋ช ๊ฐ์ง ์ค์ํ ์ฐจ์ด์ ์ด ์์ด์.
์ ๋ณดAPI ๋ผ์ฐํธ (Route Handlers): ๋ณ๋์ HTTP ์๋ํฌ์ธํธ(์:
/api/users)๋ฅผ ์ ์ํ๊ณ , ํด๋ผ์ด์ธํธ์์fetch์์ฒญ์ ๋ณด๋ด๋ ๋ฐฉ์์ด์์.์ฃผ๋ก ์ธ๋ถ ์๋น์ค์์ ์ฐ๋์ด๋ ๋ณต์กํ ๋ฐ์ดํฐ ์กฐํ ๋ก์ง์ ์ ํฉํด์.
์ ์ฉํ ํServer Actions: ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ์ง์ ํธ์ถ ๊ฐ๋ฅํ ์๋ฒ ํจ์์์.
์ฃผ๋ก ํผ ์ ์ถ, ๋ฐ์ดํฐ ๋ณ๊ฒฝ, ์ธ์ฆ๊ณผ ๊ฐ์ ํน์ ์ฌ์ฉ์ ์ธํฐ๋์ ์ ๋ํ ์๋ฒ ๋ก์ง์ ์ฒ๋ฆฌํ๋ ๋ฐ ์ต์ ํ๋์ด ์์ด์.
๋คํธ์ํฌ ์์ฒญ์ ๋ช ์์ ์ผ๋ก ์์ฑํ ํ์ ์์ด, ํจ์ ํธ์ถ๋ง์ผ๋ก ์๋ฒ ์ฝ๋๋ฅผ ์คํํ ์ ์๋ค๋ ๊ฒ์ด ๊ฐ์ฅ ํฐ ์ฅ์ ์ด์์.
Server Actions๋ POST ์์ฒญ์ ์ต์ ํ๋์ด ์์ผ๋ฉฐ, FormData ๊ฐ์ฒด๋ฅผ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ฃผ๊ธฐ ๋๋ฌธ์ ํผ ์ ์ถ ์ ๋งค์ฐ ํธ๋ฆฌํด์. ๋ํ, revalidatePath๋ revalidateTag์ ๊ฐ์ Next.js์ ์บ์ฑ ์ฌ๊ฒ์ฆ ๊ธฐ๋ฅ๊ณผ ๊ธด๋ฐํ๊ฒ ํตํฉ๋์ด ์์ด ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ์ ์งํ๊ธฐ ์ฝ๋ค๋ ์ฅ์ ๋ ์์ด์.
โ๏ธ Server Actions ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
๋ณธ๊ฒฉ์ ์ผ๋ก Server Actions๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ์์๋ณผ๊ฒ์.
0๏ธโฃ ๊ฐ๋จํ ํผ ์ ์ถ ์์
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ Server Actions๋ "use server" ์ง์์ด๋ฅผ ํ์ผ ๋งจ ์์ ์ถ๊ฐํ๊ฑฐ๋, ํจ์ ์์ ๋ถ์ฌ ์๋ฒ์์ ์คํ๋ ํจ์์์ ๋ช
์ํ๋ ๊ฒ์ผ๋ก ์์ํด์.
์ฃผ๋ก <form> ํ๊ทธ์ action ์์ฑ์ ์ง์ ์ฐ๊ฒฐํ์ฌ ์ฌ์ฉํด์.
app/actions.ts (์๋ฒ ์ก์
์ ์):
"use server"; // ์ด ํ์ผ์ ๋ชจ๋ ํจ์๋ ์๋ฒ์์ ์คํ๋ฉ๋๋ค. import { revalidatePath } from "next/cache"; interface FormData { title: string; content: string; } export async function createPost(formData: FormData) { // ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ ๋ก์ง์ ์ฌ๊ธฐ์ ๊ตฌํํด์. // ์์๋ฅผ ์ํด ๊ฐ๋จํ ์ฝ์์ ์ถ๋ ฅํ๊ณ ์ง์ฐ ์๊ฐ์ ์ค๋ดค์ด์. console.log("๊ฒ์๊ธ ์์ฑ ์์ฒญ:", formData); await new Promise((resolve) => setTimeout(resolve, 1000)); // ๋คํธ์ํฌ ์ง์ฐ ํ๋ด // ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๋ค๊ณ ๊ฐ์ ํ๊ณ , ์ฑ๊ณต ๋ฉ์์ง๋ฅผ ๋ฐํํด์. const newPost = { id: Date.now().toString(), ...formData }; console.log("์๋ก์ด ๊ฒ์๊ธ ์์ฑ ์๋ฃ:", newPost); // ์บ์๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ๊ฒ์ฆํ์ฌ UI๋ฅผ ์ ๋ฐ์ดํธํด์. revalidatePath("/posts"); // /posts ๊ฒฝ๋ก์ ์บ์๋ฅผ ๋ฌดํจํํ์ฌ ์ต์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ฒ ํด์. return { success: true, message: "๊ฒ์๊ธ์ด ์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋์์ด์.", post: newPost, }; }
app/posts/create/page.tsx (ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ํผ ์ฌ์ฉ):
"use client"; import { createPost } from "@/app/actions"; // Server Action ์ํฌํธ import { useState } from "react"; export default function CreatePostPage() { const [message, setMessage] = useState(""); // Server Action์ ์ง์ ํธ์ถํ๋ ํจ์๋ฅผ ์ ์ํด์. const handleSubmit = async (formData: FormData) => { // FormData ๊ฐ์ฒด๋ input์ name ์์ฑ์ ๊ธฐ๋ฐ์ผ๋ก ์๋์ผ๋ก ์์ฑ๋ผ์. const title = formData.get("title") as string; const content = formData.get("content") as string; // Server Action์ ํธ์ถํด์. const result = await createPost({ title, content }); setMessage(result.message); }; return ( <div className="mx-auto max-w-md rounded-lg bg-white p-4 shadow-md"> <h1 className="mb-4 text-2xl font-bold">์ ๊ฒ์๊ธ ์์ฑ</h1> {/* <form action={createPost}> ์ ๊ฐ์ด ์ง์ Server Action์ ์ฐ๊ฒฐํ ์๋ ์์ด์. */} {/* ์ฌ๊ธฐ์๋ ์ถ๊ฐ ๋ก์ง ์ฒ๋ฆฌ๋ฅผ ์ํด handleSubmit์ ์ฌ์ฉํ์ด์. */} <form action={handleSubmit}> <div className="mb-4"> <label htmlFor="title" className="mb-2 block text-sm font-bold text-gray-700" > ์ ๋ชฉ </label> <input type="text" id="title" name="title" // name ์์ฑ์ด ์ค์ํด์! className="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none" required /> </div> <div className="mb-6"> <label htmlFor="content" className="mb-2 block text-sm font-bold text-gray-700" > ๋ด์ฉ </label> <textarea id="content" name="content" // name ์์ฑ์ด ์ค์ํด์! rows={5} className="focus:shadow-outline mb-3 w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none" required ></textarea> </div> <button type="submit" className="focus:shadow-outline rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none" > ๊ฒ์๊ธ ์์ฑ </button> </form> {message && <p className="mt-4 text-green-600">{message}</p>} </div> ); }
์ ์์์์๋ createPost Server Action์ ์ง์ handleSubmit ํจ์ ๋ด์์ ํธ์ถํ์ด์. ์ด๋ ๊ฒ ํ๋ฉด ํผ ์ ์ถ ์ ํ์ ์ถ๊ฐ์ ์ธ ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๋ก์ง์ ์คํํ ์ ์์ด์.
๋๋ <form action={createPost}>์ ๊ฐ์ด action ์์ฑ์ Server Action์ ์ง์ ๋๊ฒจ์ค ์๋ ์๋ต๋๋ค.
1๏ธโฃ useFormStatus์ useFormState ํ
ํ์ฉ
Next.js๋ Server Actions์ ํจ๊ป ์ฌ์ฉํ ์ ์๋ ๋ ๊ฐ์ง ์ ์ฉํ React ํ
์ ์ ๊ณตํด์. ๋ฐ๋ก useFormStatus์ useFormState์์.
useFormStatus๋ก ๋ก๋ฉ ์ํ ๊ด๋ฆฌํ๊ธฐ
useFormStatus ํ
์ ํผ์ ์ ์ถ ์ํ(pending ์ฌ๋ถ)๋ฅผ ์๋ ค์ค์, ํผ์ด ์ ์ถ๋๋ ๋์ ๋ฒํผ์ ๋นํ์ฑํํ๊ฑฐ๋ ๋ก๋ฉ ์คํผ๋๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ฑ์ ์์
์ ํ ์ ์๊ฒ ํด์.
์ด ํ
์ ๋ฐ๋์ <form> ์ปดํฌ๋ํธ์ ์์ ์ปดํฌ๋ํธ์์ ํธ์ถ๋์ด์ผ ํด์.
app/posts/create/page.tsx (์ผ๋ถ ์์ ):
"use client"; import { createPost } from "@/app/actions"; import { useState } from "react"; +import { useFormStatus } from "react-dom"; // useFormStatus ์ํฌํธ +// ์ ์ถ ๋ฒํผ ์ปดํฌ๋ํธ +function SubmitButton() { + const { pending } = useFormStatus(); // ํผ์ ์ ์ถ ์ํ๋ฅผ ๊ฐ์ ธ์์. + + return ( + <button + type="submit" + className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline disabled:opacity-50 disabled:cursor-not-allowed" + disabled={pending} // pending ์ํ์ผ ๋ ๋ฒํผ ๋นํ์ฑํ + > + {pending ? "์์ฑ ์ค..." : "๊ฒ์๊ธ ์์ฑ"} + </button> + ); +} export default function CreatePostPage() { const [message, setMessage] = useState(""); const handleSubmit = async (formData: FormData) => { const title = formData.get("title") as string; const content = formData.get("content") as string; const result = await createPost({ title, content }); setMessage(result.message); }; return ( <div className="max-w-md mx-auto p-4 bg-white shadow-md rounded-lg"> <h1 className="text-2xl font-bold mb-4">์ ๊ฒ์๊ธ ์์ฑ</h1> <form action={handleSubmit}> <div className="mb-4"> <label htmlFor="title" className="block text-gray-700 text-sm font-bold mb-2"> ์ ๋ชฉ </label> <input type="text" id="title" name="title" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required /> </div> <div className="mb-6"> <label htmlFor="content" className="block text-gray-700 text-sm font-bold mb-2"> ๋ด์ฉ </label> <textarea id="content" name="content" rows={5} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" required ></textarea> </div> - <button - type="submit" - className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" - > - ๊ฒ์๊ธ ์์ฑ - </button> + <SubmitButton /> </form> {message && <p className="mt-4 text-green-600">{message}</p>} </div> ); }
useFormState๋ก ์๋ฒ ์๋ต ์ํ ๊ด๋ฆฌํ๊ธฐ
useFormState ํ
์ Server Action์ ์ด์ ๊ฒฐ๊ณผ(state)์ ํ์ฌ ์ ์ถ๋ ํผ ๋ฐ์ดํฐ๋ฅผ ์ธ์๋ก ๋ฐ์ ์๋ก์ด ์ํ๋ฅผ ๋ฐํํ๋ ํ
์ด์์.
์ด๋ฅผ ํตํด ์๋ฒ ์ก์
์ ์ฑ๊ณต/์คํจ ๋ฉ์์ง, ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ๋ฑ์ ํด๋ผ์ด์ธํธ์์ ํธ๋ฆฌํ๊ฒ ๊ด๋ฆฌํ ์ ์์ด์.
useFormState๋ useReducer์ ๋น์ทํ ๋ฐฉ์์ผ๋ก ๋์ํ๋ฉฐ, ์ด๊ธฐ ์ํ์ Server Action ํจ์๋ฅผ ์ธ์๋ก ๋ฐ์์.
app/posts/create/page.tsx (useFormState ์ ์ฉ):
"use client"; import { createPost } from "@/app/actions"; import { useState } from "react"; +import { useFormStatus, useFormState } from "react-dom"; // useFormState ์ํฌํธ // ... SubmitButton ์ปดํฌ๋ํธ๋ ๋์ผํด์ ... export default function CreatePostPage() { - const [message, setMessage] = useState(""); - const handleSubmit = async (formData: FormData) => { - const title = formData.get("title") as string; - const content = formData.get("content") as string; - - const result = await createPost({ title, content }); - setMessage(result.message); - }; + // useFormState ํ ์ ์ฌ์ฉํด์. ์ฒซ ๋ฒ์งธ ์ธ์๋ Server Action, ๋ ๋ฒ์งธ ์ธ์๋ ์ด๊ธฐ ์ํ์์. + // state๋ Server Action์ ๋ฐํ๊ฐ์ด์์. + const [state, formAction] = useFormState(createPost, { + success: false, + message: "", + errors: { title: "", content: "" }, + }); + return ( <div className="max-w-md mx-auto p-4 bg-white shadow-md rounded-lg"> <h1 className="text-2xl font-bold mb-4">์ ๊ฒ์๊ธ ์์ฑ</h1> - <form action={handleSubmit}> + <form action={formAction}> {/* useFormState๊ฐ ๋ฐํํ formAction์ ์ฌ์ฉํด์. */} <div className="mb-4"> <label htmlFor="title" className="block text-gray-700 text-sm font-bold mb-2"> ์ ๋ชฉ </label> <input type="text" id="title" name="title" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required /> </div> <div className="mb-6"> <label htmlFor="content" className="block text-gray-700 text-sm font-bold mb-2"> ๋ด์ฉ </label> <textarea id="content" name="content" rows={5} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" required ></textarea> </div> <SubmitButton /> </form> - {message && <p className="mt-4 text-green-600">{message}</p>} + {state.message && ( + <p className={`mt-4 ${state.success ? "text-green-600" : "text-red-600"}`}> + {state.message} + </p> + )} </div> ); }
์ด์ state.message๋ฅผ ํตํด Server Action์ ๊ฒฐ๊ณผ๋ฅผ ํ์ํ ์ ์๊ฒ ๋์์ด์. useFormState๋ Server Action์ ๋ฐํ๊ฐ์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ์ฝ๊ฒ ์ ๊ทผํ ์ ์๋๋ก ํด์ฃผ๋ ๊ฐ๋ ฅํ ๋๊ตฌ์์.
๐จ ์์ ํ Server Actions ๊ตฌํํ๊ธฐ
์ฌ์ฉ์๋ก๋ถํฐ ์ ๋ ฅ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์์ ์ฒ๋ฆฌํ ๋๋ ํญ์ ์ ํจ์ฑ ๊ฒ์ฌ์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ฒ ์ ํ ํด์ผ ํด์. ๊ทธ๋ ์ง ์์ผ๋ฉด ๋ณด์ ์ทจ์ฝ์ ์ด๋ ์๊ธฐ์น ์์ ๋ฒ๊ทธ๋ก ์ด์ด์ง ์ ์๋ต๋๋ค.
0๏ธโฃ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ (Zod ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ์ฉ)
Server Actions๋ ์๋ฒ์์ ์คํ๋๊ธฐ ๋๋ฌธ์, ํด๋ผ์ด์ธํธ์์ ํผ์ ์ ์ถํ๊ธฐ ์ ์ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ํ๋๋ผ๋ ์๋ฒ์์ ํ ๋ฒ ๋ ๊ฒ์ฆํ๋ ๊ฒ์ด ํ์์ ์ด์์.
์ฌ๊ธฐ์๋ ๊ฐ๋ ฅํ๊ณ ํ์
์์ ํ ์ ํจ์ฑ ๊ฒ์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Zod๋ฅผ ์ฌ์ฉํด๋ณผ๊ฒ์.
๋จผ์ Zod๋ฅผ ์ค์นํด์ฃผ์ธ์:
npm install zod # ๋๋ yarn add zod # ๋๋ pnpm add zod
app/actions.ts (Zod๋ฅผ ์ด์ฉํ ์ ํจ์ฑ ๊ฒ์ฌ ์ถ๊ฐ):
"use server"; import { revalidatePath } from "next/cache"; +import { z } from "zod"; // Zod ์ํฌํธ +// Zod ์คํค๋ง ์ ์: ๊ฒ์๊ธ ๋ฐ์ดํฐ์ ์ ํจ์ฑ ๊ท์น์ ์ ์ํด์. +const postSchema = z.object({ + title: z.string().min(5, "์ ๋ชฉ์ 5์ ์ด์์ด์ด์ผ ํด์.").max(100, "์ ๋ชฉ์ 100์ ์ดํ์ฌ์ผ ํด์."), + content: z.string().min(10, "๋ด์ฉ์ 10์ ์ด์์ด์ด์ผ ํด์."), +}); -interface FormData { - title: string; - content: string; -} - -export async function createPost(formData: FormData) { +export async function createPost(prevState: any, formData: FormData) { + // FormData๋ฅผ Zod ์คํค๋ง์ ๋ง๊ฒ ํ์ฑํด์. + const parsed = postSchema.safeParse({ + title: formData.get("title"), + content: formData.get("content"), + }); + + // ์ ํจ์ฑ ๊ฒ์ฌ ์คํจ ์ ์๋ฌ ๋ฐํ + if (!parsed.success) { + const fieldErrors = parsed.error.flatten().fieldErrors; + return { + success: false, + message: "์ ๋ ฅ๊ฐ์ด ์ ํจํ์ง ์์์.", + errors: { + title: fieldErrors.title?.[0] || "", + content: fieldErrors.content?.[0] || "", + }, + }; + } + + const { title, content } = parsed.data; // ์ ํจ์ฑ ๊ฒ์ฌ ํต๊ณผ๋ ๋ฐ์ดํฐ + console.log("๊ฒ์๊ธ ์์ฑ ์์ฒญ:", formData); await new Promise((resolve) => setTimeout(resolve, 1000)); const newPost = { id: Date.now().toString(), title, content }; // parsed.data ์ฌ์ฉ console.log("์๋ก์ด ๊ฒ์๊ธ ์์ฑ ์๋ฃ:", newPost); revalidatePath("/posts"); - return { success: true, message: "๊ฒ์๊ธ์ด ์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋์์ด์.", post: newPost }; + return { success: true, message: "๊ฒ์๊ธ์ด ์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋์์ด์.", errors: {}, post: newPost }; }
createPost Server Action์ด ์ด์ prevState๋ฅผ ์ฒซ ๋ฒ์งธ ์ธ์๋ก ๋ฐ๋๋ก ์์ ๋์๊ณ , FormData๋ ๋ ๋ฒ์งธ ์ธ์๋ก ๋ฐ์์. ์ด๋ useFormState ํ
์ ์๊ทธ๋์ฒ์ ๋ง์ถฐ์ง ๊ฒ์ด์์.
Zod๋ฅผ ์ฌ์ฉํ์ฌ formData์ ์ ํจ์ฑ์ ๊ฒ์ฌํ๊ณ , ์ ํจํ์ง ์์ ๊ฒฝ์ฐ ์๋ฌ ๋ฉ์์ง์ ํจ๊ป ์คํจ ์ํ๋ฅผ ๋ฐํํ๋๋ก ํ์ด์.
app/posts/create/page.tsx (์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ๋ฉ์์ง ํ์):
"use client"; import { createPost } from "@/app/actions"; import { useState } from "react"; import { useFormStatus, useFormState } from "react-dom"; interface FormState { success: boolean; message: string; errors: { title: string; content: string }; } // ... SubmitButton ์ปดํฌ๋ํธ๋ ๋์ผํด์ ... export default function CreatePostPage() { const [state, formAction] = useFormState<FormState, FormData>(createPost, { success: false, message: "", errors: { title: "", content: "" }, }); return ( <div className="max-w-md mx-auto p-4 bg-white shadow-md rounded-lg"> <h1 className="text-2xl font-bold mb-4">์ ๊ฒ์๊ธ ์์ฑ</h1> <form action={formAction}> <div className="mb-4"> <label htmlFor="title" className="block text-gray-700 text-sm font-bold mb-2"> ์ ๋ชฉ </label> <input type="text" id="title" name="title" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required /> + {state.errors.title && <p className="text-red-500 text-xs mt-1">{state.errors.title}</p>} </div> <div className="mb-6"> <label htmlFor="content" className="block text-gray-700 text-sm font-bold mb-2"> ๋ด์ฉ </label> <textarea id="content" name="content" rows={5} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" required ></textarea> + {state.errors.content && <p className="text-red-500 text-xs mt-1">{state.errors.content}</p>} </div> <SubmitButton /> </form> {state.message && ( <p className={`mt-4 ${state.success ? "text-green-600" : "text-red-600"}`}> {state.message} </p> )} </div> ); }
์ด์ ํผ ํ๋ ์๋์ ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ๋ฉ์์ง๊ฐ ํ์๋๋ ๊ฒ์ ํ์ธํ ์ ์์ด์.
1๏ธโฃ ์๋ฌ ์ฒ๋ฆฌ ์ ๋ต (try-catch, useFormState ์๋ฌ ์ํ)
Server Actions ๋ด์์ ์์์น ๋ชปํ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋๋ ์ฌ์ฉ์์๊ฒ ์ ์ ํ ํผ๋๋ฐฑ์ ์ ๊ณตํ๊ณ , ์ ํ๋ฆฌ์ผ์ด์
์ด ๋ฉ์ถ์ง ์๋๋ก ํด์ผ ํด์.
Server Actions๋ ์ผ๋ฐ์ ์ธ ๋น๋๊ธฐ ํจ์์ฒ๋ผ try-catch ๋ธ๋ก์ ์ฌ์ฉํ์ฌ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ด์.
app/actions.ts (์๋ฌ ์ฒ๋ฆฌ ์ถ๊ฐ):
"use server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; const postSchema = z.object({ title: z.string().min(5, "์ ๋ชฉ์ 5์ ์ด์์ด์ด์ผ ํด์.").max(100, "์ ๋ชฉ์ 100์ ์ดํ์ฌ์ผ ํด์."), content: z.string().min(10, "๋ด์ฉ์ 10์ ์ด์์ด์ด์ผ ํด์."), }); export async function createPost(prevState: any, formData: FormData) { const parsed = postSchema.safeParse({ title: formData.get("title"), content: formData.get("content"), }); if (!parsed.success) { const fieldErrors = parsed.error.flatten().fieldErrors; return { success: false, message: "์ ๋ ฅ๊ฐ์ด ์ ํจํ์ง ์์์.", errors: { title: fieldErrors.title?.[0] || "", content: fieldErrors.content?.[0] || "", }, }; } const { title, content } = parsed.data; + try { console.log("๊ฒ์๊ธ ์์ฑ ์์ฒญ:", formData); await new Promise((resolve) => setTimeout(resolve, 1000)); // ์๋์ ์ผ๋ก ์๋ฌ๋ฅผ ๋ฐ์์์ผ ํ ์คํธํด๋ณผ ์ ์์ด์. // if (title === "์๋ฌ") throw new Error("์๋๋ ์๋ฒ ์๋ฌ ๋ฐ์!"); const newPost = { id: Date.now().toString(), title, content }; console.log("์๋ก์ด ๊ฒ์๊ธ ์์ฑ ์๋ฃ:", newPost); revalidatePath("/posts"); return { success: true, message: "๊ฒ์๊ธ์ด ์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋์์ด์.", errors: {}, post: newPost }; + } catch (error: any) { + console.error("๊ฒ์๊ธ ์์ฑ ์ค ์๋ฒ ์๋ฌ ๋ฐ์:", error); + return { + success: false, + message: `์๋ฒ ์๋ฌ๊ฐ ๋ฐ์ํ์ด์: ${error.message || "์ ์ ์๋ ์๋ฌ"}`, + errors: { title: "", content: "" }, + }; + } }
์ด์ Server Action ๋ด์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด catch ๋ธ๋ก์์ ์ด๋ฅผ ์ก์์ useFormState๋ก ์๋ฌ ๋ฉ์์ง๋ฅผ ํด๋ผ์ด์ธํธ์ ์ ๋ฌํ ์ ์๊ฒ ๋์์ด์.
๐ ์ฌ์ฉ์ ๊ฒฝํ์ ์ํ ๋๊ด์ UI ์ ๋ฐ์ดํธ
Server Actions๋ ๋น๋๊ธฐ์ ์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์, ์๋ฒ์์ ์์
์ด ์๋ฃ๋ ๋๊น์ง ์ฝ๊ฐ์ ์ง์ฐ์ด ๋ฐ์ํ ์ ์์ด์. ์ด ์๊ฐ ๋์ ์ฌ์ฉ์์๊ฒ ์๋ฌด๋ฐ ํผ๋๋ฐฑ์ด ์๋ค๋ฉด ๋ต๋ตํจ์ ๋๋ ์ ์์ฃ .
์ด๋ ๋๊ด์ UI ์
๋ฐ์ดํธ(Optimistic UI Update) ๊ธฐ๋ฒ์ ์ฌ์ฉํ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ด์.
0๏ธโฃ ๋๊ด์ ์ ๋ฐ์ดํธ๋?
๋๊ด์ ์
๋ฐ์ดํธ๋ ์๋ฒ ์์ฒญ์ด ์ฑ๊ณตํ ๊ฒ์ด๋ผ๊ณ '๋๊ด์ ์ผ๋ก' ๊ฐ์ ํ๊ณ , ์๋ฒ ์๋ต์ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ์ฆ์ UI๋ฅผ ์
๋ฐ์ดํธํ๋ ๊ธฐ๋ฒ์ด์์.
๋ง์ฝ ์๋ฒ ์์ฒญ์ด ์คํจํ๋ฉด, ์
๋ฐ์ดํธํ๋ UI๋ฅผ ์ด์ ์ํ๋ก ๋๋๋ ค(๋กค๋ฐฑ) ์ฃผ๋ฉด ๋ผ์.
์ด๋ฅผ ํตํด ์ฌ์ฉ์๋ ๋ง์น ์ฆ์ ์์ ์ด ์๋ฃ๋ ๊ฒ์ฒ๋ผ ๋๋ผ๊ฒ ๋์ด, ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ ๋น ๋ฅด๊ณ ๋ฐ์์ฑ์ด ์ข๋ค๊ณ ์ธ์ํ๊ฒ ๋ผ์.
1๏ธโฃ useOptimistic ํ
ํ์ฉ ์์
React 18๋ถํฐ ์ ๊ณต๋๋ useOptimistic ํ
์ ์ด๋ฌํ ๋๊ด์ UI ์
๋ฐ์ดํธ๋ฅผ ์ฝ๊ฒ ๊ตฌํํ ์ ์๋๋ก ๋์์ค์.
์ด ํ
์ ํ์ฌ ์ํ(state)์ ๋๊ด์ ์ํ(optimisticState) ๋ ๊ฐ์ง๋ฅผ ๊ด๋ฆฌํด์. ์๋ฒ ์์
์ด ์งํ๋๋ ๋์ ๋๊ด์ ์ํ๋ฅผ ๋ณด์ฌ์ฃผ๋ค๊ฐ, ์๋ฒ ์๋ต์ด ์ค๋ฉด ์ค์ ์ํ๋ก ์ ํํ๋ ๋ฐฉ์์ด์ฃ .
๊ฐ๋จํ ๋๊ธ ์ถ๊ฐ ๊ธฐ๋ฅ์ ์์๋ก useOptimistic์ ์ฌ์ฉํด๋ณผ๊ฒ์.
app/comments/actions.ts (๋๊ธ ์ถ๊ฐ Server Action):
"use server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; const commentSchema = z.object({ postId: z.string(), content: z .string() .min(1, "๋๊ธ ๋ด์ฉ์ ๋น์๋ ์ ์์ด์.") .max(200, "๋๊ธ์ 200์ ์ดํ์ฌ์ผ ํด์."), }); interface Comment { id: string; postId: string; content: string; isOptimistic?: boolean; // ๋๊ด์ ์ ๋ฐ์ดํธ ์ฌ๋ถ๋ฅผ ํ์ } // ์์ ๋๊ธ ์ ์ฅ์ (์ค์ ๋ก๋ DB๋ฅผ ์ฌ์ฉํด์) const comments: Comment[] = []; export async function addComment(prevState: any, formData: FormData) { const parsed = commentSchema.safeParse({ postId: formData.get("postId"), content: formData.get("content"), }); if (!parsed.success) { return { success: false, message: parsed.error.flatten().fieldErrors.content?.[0] || "", comment: null, }; } const { postId, content } = parsed.data; try { console.log( `[Server Action] ๋๊ธ ์ถ๊ฐ ์์ฒญ: postId=${postId}, content=${content}`, ); await new Promise((resolve) => setTimeout(resolve, 1500)); // ์๋์ ์ธ ์ง์ฐ const newComment: Comment = { id: Date.now().toString(), postId, content }; comments.push(newComment); // ๋๊ธ ์ ์ฅ console.log("[Server Action] ๋๊ธ ์ถ๊ฐ ์๋ฃ:", newComment); revalidatePath(`/posts/${postId}`); // ํด๋น ๊ฒ์๊ธ์ ์บ์ ์ฌ๊ฒ์ฆ return { success: true, message: "๋๊ธ์ด ์ฑ๊ณต์ ์ผ๋ก ์ถ๊ฐ๋์์ด์.", comment: newComment, }; } catch (error: any) { console.error("[Server Action] ๋๊ธ ์ถ๊ฐ ์ค ์๋ฌ ๋ฐ์:", error); return { success: false, message: `๋๊ธ ์ถ๊ฐ ์คํจ: ${error.message || "์ ์ ์๋ ์๋ฌ"}`, comment: null, }; } }
app/posts/[id]/page.tsx (๋๊ธ ๋ชฉ๋ก ๋ฐ ์ถ๊ฐ ํผ - ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ):
"use client"; import { addComment } from "@/app/comments/actions"; import { useState, useRef, useOptimistic } from "react"; import { useFormStatus, useFormState } from "react-dom"; interface Comment { id: string; postId: string; content: string; isOptimistic?: boolean; } interface FormState { success: boolean; message: string; comment: Comment | null; } // ์์ ๋ฐ์ดํฐ (์ค์ ๋ก๋ DB์์ ๊ฐ์ ธ์์ผ ํด์) const initialComments: Comment[] = [ { id: "1", postId: "post-1", content: "์ฒซ ๋ฒ์งธ ๋๊ธ์ด์์!" }, { id: "2", postId: "post-1", content: "์ ๋ง ์ ์ตํ ๊ธ์ด๋ค์." }, ]; // ๋๊ธ ์ ์ถ ๋ฒํผ ์ปดํฌ๋ํธ function SubmitButton() { const { pending } = useFormStatus(); return ( <button type="submit" className="focus:shadow-outline rounded bg-green-500 px-4 py-2 font-bold text-white hover:bg-green-700 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" disabled={pending} > {pending ? "์ถ๊ฐ ์ค..." : "๋๊ธ ์ถ๊ฐ"} </button> ); } export default function PostDetailPage({ params }: { params: { id: string } }) { const postId = params.id; const formRef = useRef<HTMLFormElement>(null); // useOptimistic ํ ์ ์ฌ์ฉํด์. ์ฒซ ๋ฒ์งธ ์ธ์๋ ์ค์ ์ํ, ๋ ๋ฒ์งธ ์ธ์๋ ๋๊ด์ ์ ๋ฐ์ดํธ ํจ์์์. const [optimisticComments, addOptimisticComment] = useOptimistic( initialComments, // ์ด๊ธฐ ๋๊ธ ๋ชฉ๋ก (currentComments: Comment[], newCommentContent: string) => [ ...currentComments, { id: "temp-id-" + Date.now(), postId, content: newCommentContent, isOptimistic: true, }, // ๋๊ด์ ๋๊ธ ์ถ๊ฐ ], ); const [state, formAction] = useFormState<FormState, FormData>(addComment, { success: false, message: "", comment: null, }); const handleFormAction = async (formData: FormData) => { const commentContent = formData.get("content") as string; // ๋๊ด์ ์ ๋ฐ์ดํธ๋ฅผ ๋จผ์ ์ ์ฉํด์. addOptimisticComment(commentContent); // ์ค์ Server Action์ ํธ์ถํด์. const result = await formAction(formData); // Server Action์ด ์คํจํ๋ฉด (์: ์ ํจ์ฑ ๊ฒ์ฌ ์คํจ) UI๋ฅผ ๋กค๋ฐฑํ๊ฑฐ๋ ์๋ฌ ๋ฉ์์ง๋ฅผ ํ์ํด์. if (!result.success) { console.error("๋๊ธ ์ถ๊ฐ ์คํจ:", result.message); // ์ฌ๊ธฐ์๋ ๋กค๋ฐฑ ๋ก์ง์ ์ง์ ๊ตฌํํ์ง ์๊ณ , ๋ฉ์์ง๋ก๋ง ํผ๋๋ฐฑ์ ์ค์. // ์ค์ ์ฑ์์๋ `optimisticComments`์์ ํด๋น ์์ ํญ๋ชฉ์ ์ ๊ฑฐํ๋ ๋ก์ง์ด ํ์ํ ์ ์์ด์. } else { // ์ฑ๊ณตํ๋ฉด ํผ ์ด๊ธฐํ formRef.current?.reset(); } }; return ( <div className="mx-auto mt-8 max-w-xl rounded-lg bg-white p-6 shadow-md"> <h1 className="mb-6 text-3xl font-bold">๊ฒ์๊ธ ์์ธ ํ์ด์ง ({postId})</h1> <div className="mb-8"> <h2 className="mb-4 text-2xl font-semibold">๋๊ธ ๋ชฉ๋ก</h2> {optimisticComments.length === 0 ? ( <p className="text-gray-500">์์ง ๋๊ธ์ด ์์ด์.</p> ) : ( <ul> {optimisticComments.map((comment) => ( <li key={comment.id} className={`border-b border-gray-200 py-2 ${comment.isOptimistic ? "text-gray-500 italic opacity-60" : ""}`} > {comment.content} {comment.isOptimistic && ( <span className="ml-2 text-sm"> (์ ์ก ์ค...)</span> )} </li> ))} </ul> )} </div> <div className="mb-4"> <h2 className="mb-4 text-2xl font-semibold">๋๊ธ ์์ฑ</h2> <form ref={formRef} action={handleFormAction} className="flex flex-col gap-4" > <input type="hidden" name="postId" value={postId} /> <textarea name="content" rows={3} placeholder="๋๊ธ์ ์ ๋ ฅํด์ฃผ์ธ์." className="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none" required ></textarea> {state.message && ( <p className="text-sm text-red-500">{state.message}</p> )} <SubmitButton /> </form> </div> </div> ); }
์ ์์์์ addOptimisticComment ํจ์๋ฅผ ํตํด optimisticComments ์ํ๋ฅผ ์ฆ์ ์
๋ฐ์ดํธํ์ฌ ์๋ก์ด ๋๊ธ์ด UI์ ๋ฐ๋ก ๋ณด์ด๋๋ก ํ์ด์.
์ค์ ์๋ฒ ์๋ต์ด ์ค๊ธฐ ์ ๊น์ง๋ isOptimistic ํ๋๊ทธ๋ฅผ ์ด์ฉํด ์ฝ๊ฐ ํ๋ฆฌ๊ฒ ํ์ํ์ฌ '์ ์ก ์ค'์์ ์ฌ์ฉ์์๊ฒ ์๋ฆด ์ ์์ด์.
๋ง์ฝ Server Action์ด ์คํจํ๋ฉด state.message๋ฅผ ํตํด ์๋ฌ๋ฅผ ํ์ํ๊ณ , ํ์ํ๋ค๋ฉด ๋๊ด์ ์ผ๋ก ์ถ๊ฐํ๋ ํญ๋ชฉ์ ์ ๊ฑฐํ๋ ๋กค๋ฐฑ ๋ก์ง์ ์ถ๊ฐํ ์ ์์ด์.
๐ ์ ๋ฆฌํ๋ฉฐ
์ง๊ธ๊น์ง Next.js Server Actions๋ฅผ ์ค๋ฌด์ ์ ์ฉํ ๋ ํ์ํ ํต์ฌ ๊ธฐ๋ฒ๋ค์ ํจ๊ป ์ดํด๋ณด์์ด์. Server Actions๋ ์๋ฒ์ ํด๋ผ์ด์ธํธ ๊ฐ์ ๋ฐ์ดํฐ ํ๋ฆ์ ๋จ์ํํ๊ณ ๊ฐ๋ฐ ์์ฐ์ฑ์ ๋์ด๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ด์ง๋ง, ๊ทธ๋งํผ ์ฃผ์ ๊น์ ์ค๊ณ์ ๊ตฌํ์ด ํ์ํด์.
0๏ธโฃ ํต์ฌ ์์ฝ
- Server Actions: ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ์ง์ ์๋ฒ ์ฝ๋๋ฅผ ํธ์ถํ ์ ์๊ฒ ํ์ฌ ๊ฐ๋ฐ ๋ณต์ก์ฑ์ ์ค์ด๊ณ ํจ์จ์ ์ธ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ฅผ ๊ฐ๋ฅํ๊ฒ ํด์.
useFormStatus: ํผ ์ ์ถ ์ ๋ก๋ฉ ์ํ๋ฅผ ๊ด๋ฆฌํ์ฌ ์ฌ์ฉ์์๊ฒ ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํด์.useFormState: Server Action์ ๋ฐํ๊ฐ์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ์ฝ๊ฒ ์ ๊ทผํ๊ณ , ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ๋ ์๋ฒ ์๋ต ๋ฉ์์ง๋ฅผ ํ์ํ๋ ๋ฐ ์ ์ฉํด์.- ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ: Zod์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ์๋ฒ์์ ์
๋ ฅ๊ฐ์ ์ ํจ์ฑ์ ์ฒ ์ ํ ๊ฒ์ฆํ๋ ๊ฒ์ด ํ์์ ์ด์์.
- ์๋ฌ ์ฒ๋ฆฌ:
try-catch๋ธ๋ก์ ํตํด Server Actions ๋ด๋ถ์ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๊ณ ,useFormState๋ฅผ ํตํด ์ฌ์ฉ์์๊ฒ ์๋ฏธ ์๋ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ ๋ฌํด์ผ ํด์. - ๋๊ด์ UI ์
๋ฐ์ดํธ:
useOptimisticํ ์ ํ์ฉํ์ฌ ์๋ฒ ์๋ต์ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ UI๋ฅผ ์ฆ์ ์ ๋ฐ์ดํธํจ์ผ๋ก์จ ์ฌ์ฉ์ ๊ฒฝํ์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ด์.
1๏ธโฃ ๋ค์ ์ก์
Server Actions๋ Next.js ๊ฐ๋ฐ์ ๋ฏธ๋๋ฅผ ์ด๋ ์ค์ํ ๊ธฐ์ ์ค ํ๋์์. ์ด ๊ธ์์ ๋ค๋ฃฌ ๋ด์ฉ๋ค์ ๋ฐํ์ผ๋ก ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํด๋ณด์๋ฉด์, Server Actions์ ๊ฐ๋ ฅํจ์ ์ง์ ๊ฒฝํํด๋ณด์๊ธธ ์ถ์ฒํด์.
ํนํ, ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ์ ์๋ฌ ์ฒ๋ฆฌ๋ ์ฌ์ฉ์์๊ฒ ์ ๋ขฐ์ฑ ์๋ ์๋น์ค๋ฅผ ์ ๊ณตํ๊ธฐ ์ํ ํ์ ์์์ด๋, ๋ฐ๋์ ์ต๊ดํํด์ฃผ์๊ธธ ๋ฐ๋ผ์.
๊ถ๊ธํ ์ ์ด๋ ๋ ๊น์ด ๋ ผ์ํ๊ณ ์ถ์ ๋ถ๋ถ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ง ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์ธ์! ๋ค์์๋ ๋ ์ ์ตํ ์ฃผ์ ๋ก ์ฐพ์์ฌ๊ฒ์.
๐ฎ ์ฐธ๊ณ
- Next.js Docs: Server Actions and Mutations
- React Docs: useFormStatus
- React Docs: useFormState
- React Docs: useOptimistic
- Zod Documentation
์ฐ๊ด๋ ํฌ์คํธ
- ๋จ์ด: 1,996๊ฐ22๋ถ
[๐ค] Next.js App Router์ Auth.js (NextAuth.js) ์ฐ๋ ๊ฐ์ด๋: ์๋ฒ ๋ฐ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ ์ธ์ ๊ด๋ฆฌ
Next.js 14+ App Router ํ๊ฒฝ์์ Auth.js (๊ตฌ NextAuth.js)๋ฅผ ์ฌ์ฉํ์ฌ ์์ ํ๊ณ ํจ์จ์ ์ธ ์ธ์ฆ ์์คํ ์ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ์์ธํ ์์๋ด์. ์๋ฒ ์ปดํฌ๋ํธ์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ์ธ์ ์ ๊ด๋ฆฌํ๊ณ ๋ณดํธํ๋ ์ค์ฉ์ ์ธ ํ๊ณผ ์ฝ๋ ์์๋ฅผ ์ ๊ณตํด์.
- ๋จ์ด: 2,440๊ฐ26๋ถ
[๐ค] Next.js App Router์์ Zod์ TypeScript๋ก API ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ ํ์ ์์ ์ฑ ํ๋ณด
Next.js App Router ํ๊ฒฝ์์ Zod์ TypeScript๋ฅผ ํ์ฉํ์ฌ API ์์ฒญ ๋ฐ์ดํฐ์ ๋ฐํ์ ์ ํจ์ฑ์ ๊ฒ์ฌํ๊ณ , ๋์์ ๊ฐ๋ ฅํ ํ์ ์์ ์ฑ์ ํ๋ณดํ๋ ์ค์ฉ์ ์ธ ๋ฐฉ๋ฒ์ ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,646๊ฐ18๋ถ
[๐ค] JavaScript ๊ตฌ์กฐ ๋ถํด ํ ๋น ํจํด ์ฌํ: ๋ค์ํ ํ์ฉ๊ณผ ์ค์ ํ
JavaScript ๊ตฌ์กฐ ๋ถํด ํ ๋น(Destructuring Assignment)์ ๊ธฐ๋ณธ๋ถํฐ ์ค์ฒฉ ๊ฐ์ฒด, ๊ธฐ๋ณธ๊ฐ, Rest ๋ฌธ๋ฒ, ํจ์ ์ธ์ ํ์ฉ๊น์ง ๋ค์ํ ํจํด๊ณผ ์ค์ ํ์ ํตํด ์ฝ๋๋ฅผ ๋ ๊ฐ๊ฒฐํ๊ณ ํจ์จ์ ์ผ๋ก ์์ฑํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 1,841๊ฐ23๋ถ
[๐ค] ์น ์ฑ๋ฅ ์ต์ ํ: ๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ๋ก Core Web Vitals ์ง๋จํ๊ณ ๊ฐ์ ํ๊ธฐ
๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ(DevTools)์ Lighthouse, Performance, Network ํญ์ ํ์ฉํ์ฌ Core Web Vitals๋ฅผ ์ง๋จํ๊ณ ์ค์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ ๊ฐ์ ํ๋ ์ค์ง์ ์ธ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 1,957๊ฐ22๋ถ
[๐ค] TypeScript ํจ์ ์ค๋ฒ๋ก๋ฉ: ์๊ทธ๋์ฒ์ ๊ตฌํ์ฒด๋ฅผ ์๋ฒฝํ๊ฒ ์ดํดํ๊ณ ํ์ฉํ๊ธฐ
TypeScript์์ ํจ์ ์ค๋ฒ๋ก๋ฉ์ ์ฌ์ฉํ์ฌ ๋ค์ํ ํ์ ์ ์ธ์๋ฅผ ์ฒ๋ฆฌํ๊ณ , ์ฝ๋์ ๊ฐ๋ ์ฑ ๋ฐ ์์ ์ฑ์ ๋์ด๋ ๋ฐฉ๋ฒ์ ์๊ทธ๋์ฒ์ ๊ตฌํ์ฒด์ ๊ด๊ณ๋ฅผ ์ค์ฌ์ผ๋ก ์์ธํ ์ค๋ช ํด ๋๋ ค์.
- ๋จ์ด: 1,709๊ฐ19๋ถ
[๐ค] JavaScript requestIdleCallback๊ณผ isInputPending์ผ๋ก UI ๋ฐ์์ฑ ๊ฐ์ ํ๊ธฐ: ๋ฉ์ธ ์ค๋ ๋ ์ต์ ํ ์ฌํ
JavaScript์ `requestIdleCallback`๊ณผ `isInputPending` API๋ฅผ ํ์ฉํ์ฌ ๋ฉ์ธ ์ค๋ ๋ ๋ธ๋กํน์ ์ค์ด๊ณ UI ๋ฐ์์ฑ์ ๊ทน๋ํํ๋ ์ค์ง์ ์ธ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์. ์ฌ์ฉ์ ๊ฒฝํ์ ์ํ ์น ์ฑ๋ฅ ์ต์ ํ ์ ๋ต์ ์๋ดํด ๋๋ ค์.
- ๋จ์ด: 2,149๊ฐ23๋ถ
[๐ค] Next.js App Router์ `generateMetadata`์ `generateViewport` ์ฌํ: SEO ๋ฐ PWA ์ต์ ํ ์ ๋ต
Next.js 14/13 App Router์์ `generateMetadata`์ `generateViewport` ํจ์๋ฅผ ํ์ฉํ์ฌ ๋์ SEO ๋ฉํํ๊ทธ์ PWA ์ค์ ์ ์ต์ ํํ๋ ์ฌํ ์ ๋ต์ ๋ค๋ฃน๋๋ค. ์ค๋ฌด ์์์ ํจ๊ป ์น์ฌ์ดํธ์ ๊ฒ์ ์์ง ๋ ธ์ถ ๋ฐ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 1,788๊ฐ19๋ถ
[๐ค] CSS ๋ณ์(Custom Properties) ์ฌํ: ๋์ ํ ๋ง, ์ค์ฝํ, JavaScript ํ์ฉ ์ ๋ต
CSS ๋ณ์(Custom Properties)์ ๊ธฐ๋ณธ๋ถํฐ ๋์ ํ ๋ง ๊ตฌํ, ์ค์ฝํ ์ ๋ต, ๊ทธ๋ฆฌ๊ณ JavaScript์์ ์ํธ์์ฉ๊น์ง ์ค๋ฌด์ ๋ฐ๋ก ์ ์ฉํ ์ ์๋ ์ฌํ ํ์ฉ๋ฒ์ ๋ธ๋ฃจ๊ฐ ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,293๊ฐ15๋ถ
[๐ค] Next.js App Router Edge Runtime: ๊ธ๋ก๋ฒ ์ฑ๋ฅ๊ณผ ๋น์ฉ ํจ์จ์ฑ ๊ทน๋ํ ์ ๋ต
Next.js App Router์์ Edge Runtime์ ํ์ฉํด Route Handler ์ฑ๋ฅ์ ๊ทน๋ํํ๊ณ , ๊ธ๋ก๋ฒ ์ฌ์ฉ์์๊ฒ ๋น ๋ฅธ ์๋ต ์๋๋ฅผ ์ ๊ณตํ๋ฉฐ, ๋น์ฉ ํจ์จ์ ์ผ๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ ์ด์ํ๋ ์ค์ ์ ๋ต์ ์์ธํ ๋ค๋ค์. Edge Runtime์ ์ฅ์ ๊ณผ ์ ํ์ฌํญ, ๊ทธ๋ฆฌ๊ณ ์ค์ ์ฝ๋ ์์๋ฅผ ํตํด ํจ๊ณผ์ ์ธ ์ ์ฉ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 1,980๊ฐ25๋ถ
[๐ค] Next.js/React์์ ํด๋ฆฐ ์ํคํ ์ฒ์ ์์กด์ฑ ์ญ์ ์์น ์ ์ฉํ๊ธฐ: ํ์ฅ ๊ฐ๋ฅํ ํ๋ฐํธ์๋ ๋ง๋ค๊ธฐ
Next.js์ React ํ๋ก์ ํธ์์ ํด๋ฆฐ ์ํคํ ์ฒ์ ์์กด์ฑ ์ญ์ ์์น(DIP)์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃจ์ด์. ๋๋ฉ์ธ, ์ ์ค์ผ์ด์ค, ์ธํ๋ผ ๊ณ์ธต์ ๋ถ๋ฆฌํ์ฌ ์ ์ง๋ณด์์ฑ๊ณผ ํ ์คํธ ์ฉ์ด์ฑ์ ๊ทน๋ํํ๋ ์ค์ฉ์ ์ธ ์ ๋ต์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 1,733๊ฐ19๋ถ
[๐ค] Jotai๋ก Next.js App Router ํด๋ผ์ด์ธํธ ์ํ ๊ด๋ฆฌ ์ฌํ: ์ํฐ ํจํด๊ณผ ์ต์ ํ
Next.js App Router ํ๊ฒฝ์์ Jotai ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ์ฌ ํจ์จ์ ์ธ ํด๋ผ์ด์ธํธ ์ํ ๊ด๋ฆฌ ์ ๋ต์ ์์๋ณด์ธ์. ์ํฐ ํจํด์ ์ด์ฉํ ์ํ ๋ถ๋ฆฌ, ์ต์ ํ ๊ธฐ๋ฒ, ๊ทธ๋ฆฌ๊ณ ์ค์ฉ์ ์ธ ์ฝ๋ ์์๋ฅผ ํตํด ๋ณต์กํ UI ์ํ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ์ ๋ธ๋ฃจ๊ฐ ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,720๊ฐ18๋ถ
[๐ค] Next.js App Router ์ ์ญ ์ํ ๊ด๋ฆฌ: Server/Client Components ์๋ฒฝ ๊ฐ์ด๋
Next.js App Router ํ๊ฒฝ์์ Server Components์ Client Components ๊ฐ์ ์ ์ญ ์ํ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ํ๊ตฌํด์. Zustand, Recoil, Context API ๋ฑ ๋ค์ํ ์๋ฃจ์ ๊ณผ ์ค์ฉ์ ์ธ ์ ๋ต์ ํตํด ๋ณต์กํ ์ํ ๊ด๋ฆฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด ๋ณด์ธ์.
- ๋จ์ด: 1,787๊ฐ20๋ถ
[๐ค] Atomic Design ํจํด์ผ๋ก React/Next.js ์ปดํฌ๋ํธ ์ํคํ ์ฒ ํ์ฅ์ฑ ๋์ด๊ธฐ
React ๋ฐ Next.js ํ๋ก์ ํธ์์ Atomic Design ํจํด์ ์ ์ฉํ์ฌ ์ปดํฌ๋ํธ์ ์ฌ์ฌ์ฉ์ฑ๊ณผ ํ์ฅ์ฑ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ์ ์ค๋ฌด ์์์ ํจ๊ป ์์ธํ ์๋ ค๋๋ ค์. ํจ์จ์ ์ธ ํ๋ก ํธ์๋ ์ํคํ ์ฒ๋ฅผ ๊ตฌ์ถํด ๋ณด์ธ์.
- ๋จ์ด: 2,160๊ฐ24๋ถ
[๐ค] ์๋ฐ์คํฌ๋ฆฝํธ Web Workers: UI ๋ธ๋กํน ์์ด ๋ฌด๊ฑฐ์ด ์์ ์ฒ๋ฆฌํ๊ธฐ
ํ๋ก ํธ์๋์์ CPU ์ง์ฝ์ ์ธ ์์ ์ UI ๋ธ๋กํน์ ํํ ๋ฌธ์ ์์. ์๋ฐ์คํฌ๋ฆฝํธ Web Workers๋ก ๋ฉ์ธ ์ค๋ ๋๋ฅผ ํด๋ฐฉํ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ์ต์ ํํ๋ ์ค์ฉ์ ์ธ ๋ฐฉ๋ฒ์ ์ฝ๋ ์์์ ํจ๊ป ์์๋ณด์ธ์. ๋ณต์กํ ๊ณ์ฐ, ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ฑ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์์ ํ๊ฒ ์คํํ๋ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํด์.
- ๋จ์ด: 1,822๊ฐ20๋ถ
[๐ค] TypeScript `as const` ์ฌ์ธต ๋ถ์: ๋ฆฌํฐ๋ด ํ์ ์ถ๋ก ๊ณผ ๋ถ๋ณ์ฑ ํ์ฉ ์ ๋ต
TypeScript์ `as const`๋ฅผ ํ์ฉํ์ฌ ๋ฆฌํฐ๋ด ํ์ ์ถ๋ก ์ ๊ฐํํ๊ณ , ๋ฐฐ์ด๊ณผ ๊ฐ์ฒด์ ๋ถ๋ณ์ฑ์ ์์ ํ๊ฒ ํ๋ณดํ๋ ์ค์ฉ์ ์ธ ๋ฐฉ๋ฒ์ ์์ธํ ์์๋ด์. ์ค๋ฌด ์ฝ๋์ ํ์ ์์ ์ฑ๊ณผ ๊ฐ๋ ์ฑ์ ๋์ฌ๋ณด์ธ์.
- ๋จ์ด: 1,486๊ฐ17๋ถ
[๐ค] Next.js @next/font๋ฅผ ํ์ฉํ ํฐํธ ์ต์ ํ ๋ฐ CLS ๊ฐ์ ์ ๋ต
Next.js ์ ํ๋ฆฌ์ผ์ด์ ์์ ํฐํธ ๋ก๋ฉ์ผ๋ก ์ธํ Cumulative Layout Shift(CLS)๋ฅผ ์ต์ํํ๊ณ ์น ์ฑ๋ฅ์ ๊ทน๋ํํ๋ @next/font ์ฌ์ฉ๋ฒ๊ณผ ์ค์ฉ์ ์ธ ์ต์ ํ ์ ๋ต์ ์์ธํ ์์๋ด์.
- ๋จ์ด: 1,914๊ฐ21๋ถ
[๐ค] Next.js App Router ๊ตญ์ ํ (i18n) ์๋ฒฝ ๊ฐ์ด๋: ๋ค๊ตญ์ด ์ง์ ๊ตฌํ ์ ๋ต
Next.js 14+ App Router ํ๊ฒฝ์์ ๊ตญ์ ํ(i18n)๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์์ธํ ์๋ ค๋๋ ค์. ๋ฏธ๋ค์จ์ด๋ฅผ ํ์ฉํ ์ธ์ด ๊ฐ์ง๋ถํฐ ํด๋ผ์ด์ธํธ/์๋ฒ ์ปดํฌ๋ํธ์์์ ๋ค๊ตญ์ด ํ ์คํธ ์ฒ๋ฆฌ๊น์ง, ์ค๋ฌด์ ๋ฐ๋ก ์ ์ฉํ ์ ์๋ ๋ค๊ตญ์ด ์ง์ ์ ๋ต์ ๋ธ๋ฃจ๊ฐ ์๋ดํด ๋๋ฆด๊ฒ์.
- ๋จ์ด: 1,328๊ฐ16๋ถ
[์น ์ฑ๋ฅ ์ต์ ํ] Interaction to Next Paint (INP) ๊ฐ์ : ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํ๋ ์ค์ ์ ๋ต
์๋ก์ด Core Web Vitals ์งํ, Interaction to Next Paint(INP)๋ฅผ ๊น์ด ์ดํดํ๊ณ ์ค์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ INP ์ ์๋ฅผ ๊ฐ์ ํ๋ ์ค์ฉ์ ์ธ ์ต์ ํ ์ ๋ต๋ค์ ๋ธ๋ฃจ๊ฐ ์์ธํ ์๋ ค๋๋ ค์. ์ฌ์ฉ์ ์ธํฐ๋์ ์๋ต์ฑ์ ๋์ฌ ์ง์ ํ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ง๋ค์ด ๋ณด์ธ์.
๋จ์ด: 1,668๊ฐ20๋ถ[๐ค] JavaScript Decorator ์ฌ์ธต ๋ถ์: ๋ฉํํ๋ก๊ทธ๋๋ฐ๊ณผ ํ์ ์คํฌ๋ฆฝํธ ํ์ฉ ์ ๋ต
JavaScript Decorator์ ๊ฐ๋ ๋ถํฐ ํ์ ์คํฌ๋ฆฝํธ์์์ ํ์ฉ, ๊ทธ๋ฆฌ๊ณ ๋ฏธ๋ ํ์คํ ๋ฐฉํฅ๊น์ง ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃจ๋ฉฐ, ์ค๋ฌด์์ ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ๊ณผ ๊ฐ๋ ์ฑ์ ๋์ด๋ ๋ฉํํ๋ก๊ทธ๋๋ฐ ํจํด์ ํ์ตํด ๋ณด์ธ์.
- ๋จ์ด: 2,634๊ฐ28๋ถ
[๐ค] JavaScript ํ๋ก๋ฏธ์ค/async-await ์ฌํ: ์ค์ ํจํด๊ณผ ์ฃผ์์ฌํญ
JavaScript์ ํ๋ก๋ฏธ์ค์ async/await๋ฅผ ๊น์ด ์ดํดํ๊ณ , ์ค๋ฌด์์ ๋ง์ฃผํ๋ ๋ณต์กํ ๋น๋๊ธฐ ์ฒ๋ฆฌ ์ํฉ์์ ํ์ฉํ ์ ์๋ ๊ณ ๊ธ ํจํด๊ณผ ํํ ๋ฐ์ํ๋ ์ค์๋ฅผ ๋ฐฉ์งํ๋ ๋ฐฉ๋ฒ์ ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,421๊ฐ16๋ถ
[๐ค] TypeScript 5.2+ `using` ์ ์ธ์ผ๋ก ์์ ์๋ ๊ด๋ฆฌํ๊ธฐ: ๊น๋ํ ๋ฆฌ์์ค ์ ๋ฆฌ์ ์์
TypeScript 5.2์์ ๋์ ๋ `using` ์ ์ธ์ ํ์ฉํ์ฌ ํ์ผ ํธ๋ค, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ๋ฑ ๋ค์ํ ์์์ ์๋์ผ๋ก ๊ด๋ฆฌํ๊ณ ๊น๋ํ๊ฒ ์ ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ค์ฉ์ ์ธ ์์ ์ ํจ๊ป ์์ธํ ์์๋ด์. ๋ ์ด์ `try...finally` ๋ธ๋ก์ผ๋ก ๋ณต์กํ๊ฒ ์์์ ํด์ ํ ํ์ ์์ด์.
- ๋จ์ด: 2,006๊ฐ22๋ถ
[๐ค] Feature-Sliced Design (FSD)์ผ๋ก ํ์ฅ ๊ฐ๋ฅํ ํ๋ก ํธ์๋ ์ํคํ ์ฒ ๊ตฌ์ถํ๊ธฐ
Feature-Sliced Design (FSD)์ ๋๊ท๋ชจ ํ๋ก ํธ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ํ์ฅ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ๋์ด๋ ๊ฐ๋ ฅํ ์ํคํ ์ฒ ํจํด์ด์์. FSD์ ํต์ฌ ์์น, ๋ ์ด์ด ๊ตฌ์กฐ, ์ค๋ฌด ์ ์ฉ ๋ฐฉ๋ฒ์ ์ฝ๋ ์์์ ํจ๊ป ์์ธํ ์์๋ด์.
- ๋จ์ด: 1,781๊ฐ21๋ถ
[๐ค] TypeScript `moduleResolution: 'bundler'`์ `verbatimModuleSyntax` ์๋ฒฝ ์ดํด: ๋ชจ๋ ๋ฒ๋ค๋ฌ ์๋์ ํ์ ์คํฌ๋ฆฝํธ ์ค์ ์ต์ ํ
๋ชจ๋ ์๋ฐ์คํฌ๋ฆฝํธ ํ๋ก์ ํธ์์ TypeScript์ `moduleResolution: 'bundler'`์ `verbatimModuleSyntax` ์ต์ ์ด ์ ์ค์ํ์ง, ์ด๋ป๊ฒ ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ํ๊ณ ํ์ฉํ์ฌ ๊ฐ๋ฐ ๊ฒฝํ๊ณผ ๋ฒ๋ค๋ง ํจ์จ์ ๊ทน๋ํํ๋์ง ์ค๋ฌด ์์์ ํจ๊ป ์์ธํ ์์๋ด์.
- ๋จ์ด: 1,908๊ฐ24๋ถ
[๐ค] ๋ชจ๋ ธ๋ ํฌ ์ํคํ ์ฒ: ํจ์จ์ ์ธ ๋๊ท๋ชจ ํ๋ก์ ํธ ์ค๊ณ๋ฅผ ์ํ ์ค์ ๊ฐ์ด๋
๋๊ท๋ชจ ์น ํ๋ก์ ํธ์์ ๋ชจ๋ ธ๋ ํฌ๋ฅผ ๋์ ํ๋ ์ด์ , ์ฅ๋จ์ , ๊ทธ๋ฆฌ๊ณ ํจ๊ณผ์ ์ธ ๊ตฌ์กฐ ์ค๊ณ ๋ฐ ์ค๋ฌด ์ ์ฉ ์ ๋ต์ ๋ธ๋ฃจ๊ฐ ์์ธํ ์๋ ค๋๋ ค์. ํ ์์ฐ์ฑ ํฅ์๊ณผ ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
๋จ์ด: 2,188๊ฐ26๋ถ[๐ค] ์น ์ฑ๋ฅ ์ต์ ํ: ๋ฆฌํ๋ก์ฐ์ ๋ฆฌํ์ธํธ ์ต์ํ ์ ๋ต
๋ธ๋ผ์ฐ์ ๋ ๋๋ง ๊ณผ์ ์ ํต์ฌ์ธ ๋ฆฌํ๋ก์ฐ(Reflow)์ ๋ฆฌํ์ธํธ(Repaint)์ ๋ฐ์ ์๋ฆฌ๋ฅผ ์ดํดํ๊ณ , ์ด๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ค์ฌ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ ๊ทน๋ํํ๋ ์ค์ง์ ์ธ ์ ๋ต๊ณผ ์ฝ๋ ์์๋ฅผ ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,903๊ฐ19๋ถ
[๐ค] Vitest์ React Testing Library๋ก Next.js ์ปดํฌ๋ํธ ์๋ฒฝ ํ ์คํธํ๊ธฐ
Next.js ํ๋ก์ ํธ์์ Vitest์ React Testing Library๋ฅผ ํ์ฉํ์ฌ UI ์ปดํฌ๋ํธ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ ์คํธํ๋ ๋ฐฉ๋ฒ์ ์ค๋ฌด ์์ ์ ํจ๊ป ์์ธํ ์์๋ด์. ์ค์ ๋ถํฐ Mocking, ์ด๋ฒคํธ ์๋ฎฌ๋ ์ด์ ๊น์ง, ๊ฒฌ๊ณ ํ ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ์ํ ํ ์คํธ ์ ๋ต์ ์ตํ๋ณด์ธ์.
- ๋จ์ด: 1,542๊ฐ20๋ถ
[๐ค] Tailwind CSS v4 ์ถ์: ๊ฐ๋ฐ์์๊ฒ ์ฐพ์์ฌ ๋ณํ์ ์ต์ ํ ์ ๋ต
Tailwind CSS v4์ ์ฃผ์ ๋ณ๊ฒฝ์ฌํญ๊ณผ ์๋ก์ด ๊ธฐ๋ฅ๋ค์ ๊น์ด ์๊ฒ ๋ถ์ํ๊ณ , ์ค๋ฌด์์ ํจ์จ์ ์ผ๋ก ์ ์ฉํ๋ฉฐ ์ฑ๋ฅ์ ์ต์ ํํ๋ ์ ๋ต์ ๋ธ๋ฃจ๊ฐ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,566๊ฐ20๋ถ
[๐ค] Next.js Dockerfile ์ต์ ํ: ํ๋ก๋์ ๋ฐฐํฌ๋ฅผ ์ํ ์๋ฒฝ ๊ฐ์ด๋
Next.js ์ ํ๋ฆฌ์ผ์ด์ ์ Docker ์ปจํ ์ด๋๋ก ํจ์จ์ ์ผ๋ก ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. ๋ฉํฐ์คํ ์ด์ง ๋น๋, ์บ์ฑ ์ ๋ต, ๋ณด์ ์ค์ ๋ฑ ํ๋ก๋์ ํ๊ฒฝ์ ์ต์ ํ๋ Dockerfile ์์ฑ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํด ๋๋ ค์.
- ๋จ์ด: 2,177๊ฐ24๋ถ
[๐ค] CSS Grid ์ฌํ ๊ฐ์ด๋: ์ค์ ๋ ์ด์์ ํจํด๊ณผ ๋ฐ์ํ ๋์์ธ ์ ๋ต
CSS Grid๋ ๊ฐ๋ ฅํ 2์ฐจ์ ๋ ์ด์์ ์์คํ ์ด์์. ์ด ๊ฐ์ด๋์์ Grid์ ํต์ฌ ๊ฐ๋ ๋ถํฐ ์ค์ ๋ ์ด์์ ํจํด, ๋ฐ์ํ ๋์์ธ ์ ๋ต๊น์ง ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃจ์ด ์ค๋ฌด์ ๋ฐ๋ก ์ ์ฉํ ์ ์๋๋ก ๋์๋๋ ค์.
- ๋จ์ด: 1,718๊ฐ19๋ถ
[๐ค] TypeScript ํ ํ๋ฆฟ ๋ฆฌํฐ๋ด ํ์ : ๋ฌธ์์ด ํ์ ์ ๋ง๋ฒ์ฌ๋ก ๋ณ์ ํ๊ธฐ
TypeScript์ ํ ํ๋ฆฟ ๋ฆฌํฐ๋ด ํ์ ์ ํ์ฉํ์ฌ ๋ณต์กํ ๋ฌธ์์ด ํจํด์ ์์ ํ๊ฒ ํ์ ์ถ๋ก ํ๊ณ , ๊ฐ๋ ฅํ ์ ํธ๋ฆฌํฐ ํ์ ์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์ค๋ฌด ์์ ์ ํจ๊ป ์์ธํ ์์๋ด์. ํ์ ์์ ์ฑ์ ํ ๋จ๊ณ ๋์ฌ ๊ฐ๋ฐ ๊ฒฝํ์ ๊ฐ์ ํด ๋ณด์ธ์.
- ๋จ์ด: 1,918๊ฐ23๋ถ
[๐ค] JavaScript WeakMap๊ณผ WeakSet: ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง์ ์ต์ ํ ์ ๋ต
JavaScript์์ WeakMap๊ณผ WeakSet์ ํ์ฉํ์ฌ ๋ฉ๋ชจ๋ฆฌ ๋์๋ฅผ ๋ฐฉ์งํ๊ณ ์ฑ๋ฅ์ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์. ๊ฐ๋น์ง ์ปฌ๋ ์ ๋์ ์๋ฆฌ์ ํจ๊ป ๊ฐ์ฒด ์ฐธ์กฐ ๊ด๋ฆฌ์ ์ค์์ฑ์ ์ดํดํ๊ณ , ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ์ ๋ต์ ๋ฐฐ์๋ด์.
- ๋จ์ด: 1,455๊ฐ17๋ถ
[๐ค] Next.js/React ํ๋ก์ ํธ๋ฅผ ์ํ ESLint & Prettier ์ค์ ์๋ฒฝ ๊ฐ์ด๋
๋ณต์กํ Next.js ๋ฐ React ํ๋ก์ ํธ์์ ์ผ๊ด๋ ์ฝ๋ ์คํ์ผ๊ณผ ํ์ง์ ์ ์งํ๋ ESLint์ Prettier ์ค์ ๋ฐฉ๋ฒ์ ์์ธํ ์๋ ค๋๋ ค์. ํ ๊ฐ๋ฐ ํ๊ฒฝ์ ์ต์ ํ๋ ์ค์ ์ผ๋ก ๊ฐ๋ฐ ํจ์จ์ ๋์ฌ๋ณด์ธ์.
- ๋จ์ด: 2,022๊ฐ25๋ถ
[๐ค] JavaScript์ ํต์ฌ: ํ๋กํ ํ์ ์ฒด์ธ ์๋ฒฝ ์ดํด์ ํ์ฉ ์ ๋ต
JavaScript์ ์ฌ์ฅ๋ถ, ํ๋กํ ํ์ ์ฒด์ธ์ ๋์ ์๋ฆฌ๋ฅผ ๊น์ด ํ๊ณ ๋ค์ด ๊ฐ์ฒด ์งํฅ ํ๋ก๊ทธ๋๋ฐ๊ณผ ์์์ ์๋ฒฝํ๊ฒ ์ดํดํ๊ณ ์ค๋ฌด์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์. ์ฑ๋ฅ ์ต์ ํ ํ๋ ํจ๊ป ๋ค๋ค์.
- ๋จ์ด: 2,118๊ฐ22๋ถ
[๐ค] React ์ปค์คํ ํ : ์ฌ์ฌ์ฉ์ฑ ๋์ด๋ ์ค๊ณ ์์น๊ณผ ํ ์คํธ ์ ๋ต
React ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฌ์ฉ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ๊ทน๋ํํ๋ ์ปค์คํ ํ ์ค๊ณ ์์น, ์ค์ฉ์ ์ธ ํจํด, ๊ทธ๋ฆฌ๊ณ ๊ฒฌ๊ณ ํ ํ ์คํธ ์ ๋ต์ ์๋์ด ๊ฐ๋ฐ์์ ๊ด์ ์์ ์์ธํ ์ค๋ช ํด ๋๋ ค์.
- ๋จ์ด: 2,107๊ฐ23๋ถ
[๐ค] React useRef ํ ์ฌ์ธต ๋ถ์: DOM ๋์ด์ ์ค์ ํ์ฉ ์ ๋ต
React useRef ํ ์ ๊ธฐ๋ณธ ์๋ฆฌ๋ถํฐ DOM ์์ ์ง์ ์ ์ด, ์ปดํฌ๋ํธ ๋ผ์ดํ์ฌ์ดํด ๊ด๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํ ๋ค์ํ ์ค์ ํ์ฉ ์ ๋ต๊น์ง ์ฌ์ธต์ ์ผ๋ก ๋ค๋ค์. ์ด์ค๊ธ ๊ฐ๋ฐ์๋ฅผ ์ํ useRef ์๋ฒฝ ๊ฐ์ด๋.
- ๋จ์ด: 1,762๊ฐ19๋ถ
[๐ค] Next.js 14/15์์ ๋์ OG ์ด๋ฏธ์ง ์์ฑ: ImageResponse ์๋ฒฝ ๊ฐ์ด๋
Next.js App Router ํ๊ฒฝ์์ ImageResponse๋ฅผ ํ์ฉํ์ฌ ๋์ OG ์ด๋ฏธ์ง๋ฅผ ํจ์จ์ ์ผ๋ก ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. SEO์ ์์ ๊ณต์ ์ต์ ํ๋ฅผ ์ํ ์ค์ ๊ฐ์ด๋์ ๋๋ค.
- ๋จ์ด: 1,481๊ฐ18๋ถ
[๐ค] Git ๋ธ๋์น ์ ๋ต: Git Flow vs GitHub Flow, ์ค๋ฌด์์ ์ด๋ป๊ฒ ์ ํํ๊ณ ์ด์ํ ๊น์?
๊ฐ๋ฐํ์ ํจ์จ์ ์ธ ํ์ ์ ์ํ Git ๋ธ๋์น ์ ๋ต์ ๊ณ ๋ฏผํ๊ณ ๊ณ์ ๊ฐ์? Git Flow์ GitHub Flow์ ํต์ฌ ๊ฐ๋ ๋ถํฐ ์ฅ๋จ์ , ๊ทธ๋ฆฌ๊ณ ์ฐ๋ฆฌ ํ์ ๋ง๋ ์ ๋ต์ ์ ํํ๊ณ ์ด์ํ๋ ์ค์ง์ ์ธ ํ๊น์ง '๋ธ๋ฃจ'๊ฐ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,442๊ฐ16๋ถ
[๐ค] TypeScript ํ์ ๊ฐ๋: ๋ฐํ์ ํ์ ์์ ์ฑ์ ์ํ ํ์ ํจํด ์ ๋ณตํด์
TypeScript์์ ๋ฐํ์์ ๋ณ์์ ํ์ ์ ์์ ํ๊ฒ ์ขํ๋(Narrowing) ๋ฐฉ๋ฒ์ธ ํ์ ๊ฐ๋(Type Guard)์ ๋ํด ์์ธํ ์์๋ด์. `typeof`, `instanceof`, `in` ์ฐ์ฐ์๋ถํฐ ์ฌ์ฉ์ ์ ์ ํ์ ๊ฐ๋๊น์ง, ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ๊ฒฌ๊ณ ํ ์ฝ๋๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์ตํ๋ด์.
- ๋จ์ด: 2,542๊ฐ28๋ถ
[๐ค] React Query (TanStack Query) ์ฌํ: ๋ฐ์ดํฐ ํ์นญ, ์บ์ฑ, ๋๊ธฐํ ์ ๋ต์ผ๋ก ์น ์ฑ ์ฑ๋ฅ ๊ทน๋ํํด์
React Query (TanStack Query)๋ฅผ ํ์ฉํ์ฌ ๋ณต์กํ ์๋ฒ ์ํ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ , ์ง๋ฅ์ ์ธ ์บ์ฑ๊ณผ ์๋ ๋๊ธฐํ ์ ๋ต์ผ๋ก ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃจ์ด์. useQuery, useMutation, useInfiniteQuery ๋ฑ ํต์ฌ ํ ๊ณผ ์ค์ ์ต์ ํ ํ์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 2,401๊ฐ26๋ถ
[๐ค] React `useTransition`๊ณผ `useDeferredValue`๋ก ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํ๋ ๋ฐฉ๋ฒ
React ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ฌด๊ฑฐ์ด UI ์ ๋ฐ์ดํธ๋ก ์ธํ ๋ฒ๋ฒ ์์ ํด๊ฒฐํ๊ณ , `useTransition`๊ณผ `useDeferredValue` ํ ์ ํ์ฉํ์ฌ ์ฌ์ฉ์ ๊ฒฝํ์ ํ๊ธฐ์ ์ผ๋ก ๊ฐ์ ํ๋ ์ค์ฉ์ ์ธ ์ ๋ต์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 1,917๊ฐ22๋ถ
[๐ค] React Suspense์ ErrorBoundary: ๊ฒฌ๊ณ ํ๊ณ ๋ถ๋๋ฌ์ด UI ๊ฒฝํ์ ์ํ ์ค์ ๊ฐ์ด๋
React ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฉ์ ๊ฒฝํ์ ํ์ ํ Suspense์ ErrorBoundary์ ๊ฐ๋ ฅํ ์กฐํฉ์ ๊น์ด ์๊ฒ ๋ค๋ค์. ๋ก๋ฉ ์ํ์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ฐ์ํ๊ฒ ๊ด๋ฆฌํ์ฌ ๋์ฑ ๊ฒฌ๊ณ ํ๊ณ ๋ถ๋๋ฌ์ด UI๋ฅผ ๋ง๋๋ ์ค์ ํ๊ณผ ์ฝ๋ ์์๋ฅผ ํ์ธํด ๋ณด์ธ์.
- ๋จ์ด: 1,302๊ฐ16๋ถ
[๐ค] CSS Container Queries: ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ๋ฐ์ํ ๋์์ธ์ ์๋ก์ด ์งํ
๋ฏธ๋์ด ์ฟผ๋ฆฌ์ ํ๊ณ๋ฅผ ๋์ด, ์ปดํฌ๋ํธ ์์ฒด์ ํฌ๊ธฐ์ ๋ฐ๋ผ ์คํ์ผ์ ์กฐ์ ํ๋ CSS Container Queries๋ฅผ ๊น์ด ์๊ฒ ์์๋ณด๊ณ ์ค๋ฌด ์ ์ฉ ๋ฐฉ๋ฒ์ ์๋ดํด ๋๋ ค์.
- ๋จ์ด: 1,681๊ฐ19๋ถ
[๐ค] Next.js 15 ๊ณ ๊ธ ๋ฐ์ดํฐ ์บ์ฑ ์ ๋ต: fetch์ revalidate ์ฌ์ธต ๋ถ์
Next.js 15์์ `fetch` API์ ๊ฐ๋ ฅํ ์บ์ฑ ๋ฉ์ปค๋์ฆ๊ณผ `revalidate` ์ต์ ์ ํ์ฉํ์ฌ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ ์ต์ ํํ๊ณ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃจ์ด์. ์ค๋ฌด ์์๋ฅผ ํตํด ์๋ฒ ์ปดํฌ๋ํธ์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ์บ์ฑ ์ ๋ต์ ํจ๊ณผ์ ์ผ๋ก ์ ์ฉํ๋ ํ์ ์ ๊ณตํด์.
๋จ์ด: 1,320๊ฐ14๋ถ[๐ค] Next.js App Router: generateStaticParams๋ก ๋์ ๋ผ์ฐํ ๋น๋ ์ต์ ํํ๊ธฐ
Next.js App Router์์ generateStaticParams ํจ์๋ฅผ ํ์ฉํ์ฌ ๋์ ๋ผ์ฐํ ์ ์ ์ ํ์ด์ง๋ฅผ ํจ์จ์ ์ผ๋ก ์์ฑํ๊ณ ๋น๋ ์ฑ๋ฅ์ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์.
๋จ์ด: 1,891๊ฐ22๋ถ[๐ค] React ๋ ๋๋ง ์ต์ ํ: useMemo, useCallback, React.memo ์๋ฒฝ ๊ฐ์ด๋
์ด์ค๊ธ ๊ฐ๋ฐ์๋ฅผ ์ํ React ๋ ๋๋ง ์ต์ ํ ๊ฐ์ด๋. useMemo, useCallback, React.memo์ ์ ํํ ์ฌ์ฉ๋ฒ๊ณผ ์ค๋ฌด์์ ํํ ์ ์ง๋ฅด๋ ์ค์, ๊ทธ๋ฆฌ๊ณ ์ค์ ์ฑ๋ฅ ํฅ์ ์ ๋ต์ ๋ธ๋ฃจ๊ฐ ์๋ ค๋๋ ค์.
๋จ์ด: 2,145๊ฐ24๋ถ[๐ค] JavaScript Proxy์ Reflect ์ฌ์ธต ๋ถ์: ๋ฉํ ํ๋ก๊ทธ๋๋ฐ์ผ๋ก ์ฝ๋ ๊ฐํํ๊ธฐ
JavaScript Proxy์ Reflect API๋ฅผ ํ์ฉํ ๋ฉํ ํ๋ก๊ทธ๋๋ฐ ๊ธฐ๋ฒ์ ์ฌ์ธต ๋ถ์ํด์. ๊ฐ์ฒด ์ ๊ทผ ์ ์ด, ์ ํจ์ฑ ๊ฒ์ฌ, ๋ก๊น , ๋ฐ์ํ ์์คํ ๊ตฌํ ๋ฑ ์ค์ฉ์ ์ธ ํ์ฉ ์ฌ๋ก๋ฅผ ํตํด ์ฝ๋์ ์ ์ฐ์ฑ๊ณผ ์์ ์ฑ์ ๋์ด๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
- ๋จ์ด: 2,029๊ฐ24๋ถ
[๐ค] React/Next.js ๋ฒ๋ค ์ต์ ํ: ์ฝ๋ ์คํ๋ฆฌํ ๊ณผ ๋ ์ด์ง ๋ก๋ฉ ์๋ฒฝ ๊ฐ์ด๋
React์ Next.js ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฒ๋ค ํฌ๊ธฐ๋ฅผ ์ค์ด๊ณ ๋ก๋ฉ ์๋๋ฅผ ๊ฐ์ ํ๋ ์ฝ๋ ์คํ๋ฆฌํ ๊ณผ ๋ ์ด์ง ๋ก๋ฉ ๊ธฐ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์. ์นํฉ ์ค์ ๋ถํฐ React.lazy, Next.js dynamic import๊น์ง ๋ค๋ค์.
- ๋จ์ด: 1,770๊ฐ20๋ถ
[๐ค] React์ `useOptimistic` ํ ์ผ๋ก ๋๊ด์ UI ์ ๋ฐ์ดํธ ๊ตฌํํ๊ธฐ: Server Actions์ ํจ๊ป
React 18/19์ `useOptimistic` ํ ์ ํ์ฉํ์ฌ Server Actions์ ์ฐ๋๋๋ ๋๊ด์ UI ์ ๋ฐ์ดํธ๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ค์ฉ์ ์ธ ์์์ ํจ๊ป ์์ธํ ์์๋ด์. ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๊ณ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฐ์์ฑ์ ๋์ด๋ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํด์.
๋จ์ด: 1,561๊ฐ17๋ถ[๐ค] TypeScript const Type Parameters: ๋ฆฌํฐ๋ด ํ์ ์ถ๋ก ๊ฐํ์ ์ค์ฉ์ ์ธ ํ์ฉ๋ฒ
TypeScript 5.0์ ๋์ ๋ const Type Parameters๋ฅผ ํ์ฉํ์ฌ ์ ๋ค๋ฆญ ํจ์์ ๋ฆฌํฐ๋ด ํ์ ์ถ๋ก ์ ์ ๊ตํ๊ฒ ์ ์ดํ๊ณ , ๋์ฑ ๊ฒฌ๊ณ ํ ํ์ ์์คํ ์ ๊ตฌ์ถํ๋ ์ค์ฉ์ ์ธ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. as const์์ ์ฐจ์ด์ ๊ณผ ์ค์ ์ฝ๋ ์์๋ฅผ ํตํด ์ด์ค๊ธ ๊ฐ๋ฐ์๋ ์ฝ๊ฒ ์ดํดํ ์ ์๋๋ก ์ค๋ช ํด ๋๋ ค์.
- ๋จ์ด: 2,028๊ฐ22๋ถ
[๐ค] Next.js/React ์ฑ CLS ์ต์ ํ: ์ํํธ ์๋ ์ฌ์ฉ์ ๊ฒฝํ ๋ง๋ค๊ธฐ
Next.js์ React ์ ํ๋ฆฌ์ผ์ด์ ์์ Cumulative Layout Shift(CLS) ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๋ ์ค์ง์ ์ธ ์ ๋ต๊ณผ ์ฝ๋ ์์๋ฅผ ์์ธํ ์์๋ณด์ธ์. ์น ์ฑ๋ฅ ์ต์ ํ์ ํต์ฌ ์์์ธ CLS๋ฅผ ํจ๊ณผ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,740๊ฐ21๋ถ
[๐ค] Next.js SSR, SSG, ISR ๋ ๋๋ง ์ ๋ต: App Router์์ ์ต์ ์ ์ ํ์?
Next.js App Router์์ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR), ์ ์ ์ฌ์ดํธ ์์ฑ(SSG), ์ฆ๋ถ ์ ์ ์ฌ์์ฑ(ISR) ๊ฐ ๋ ๋๋ง ์ ๋ต์ ๋์ ์๋ฆฌ, ์ฅ๋จ์ , ์ค์ ํ์ฉ ๋ฐ ์ต์ ํ ๋ฐฉ๋ฒ์ ๋น๊ต ๋ถ์ํด๋๋ ค์.
- ๋จ์ด: 1,478๊ฐ17๋ถ
[๐ค] React Context API์ Zustand: ์ ์ญ ์ํ ๊ด๋ฆฌ, ์ธ์ ๋ฌด์์ ์จ์ผ ํ ๊น์?
React ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ณ ๋ฏผํ๊ณ ๊ณ์ ๊ฐ์? Context API์ ๊ฐ๋ฒผ์ด ์ธ๋ถ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Zustand๋ฅผ ๋น๊ต ๋ถ์ํ๊ณ , ์ค๋ฌด์์ ๊ฐ ๋๊ตฌ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ์ฉํ๋ ์ ๋ต์ ์ค์ ์ฝ๋ ์์์ ํจ๊ป ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 2,004๊ฐ24๋ถ
[๐ค] Turborepo๋ก Next.js ๋ชจ๋ ธ๋ ํฌ ๊ตฌ์ถ: ํจ์จ์ ์ธ ๊ฐ๋ฐ ๋ฐ ์ต์ ํ ์ ๋ต
Turborepo๋ฅผ ํ์ฉํ์ฌ Next.js ํ๋ก์ ํธ๋ฅผ ๋ชจ๋ ธ๋ ํฌ๋ก ๊ตฌ์ฑํ๊ณ , ๊ณต์ ์ปดํฌ๋ํธ, ์ ํธ๋ฆฌํฐ, CI/CD ์ต์ ํ ๋ฐฉ์์ ์ค๋ฌด ์์์ ํจ๊ป ์์ธํ ์ค๋ช ํด ๋๋ ค์.
- ๋จ์ด: 2,338๊ฐ27๋ถ
[๐ค] React useEffect ํ , ์ด์ ํท๊ฐ๋ฆฌ์ง ๋ง์ธ์! (์์กด์ฑ ๋ฐฐ์ด, ํด๋ฆฐ์ ์๋ฒฝ ๊ฐ์ด๋)
React ๊ฐ๋ฐ์์ ํ์์ ์ธ useEffect ํ ์ ๋์ ์๋ฆฌ๋ถํฐ ์์กด์ฑ ๋ฐฐ์ด, ํด๋ฆฐ์ ํจ์ ํ์ฉ๋ฒ, ๊ทธ๋ฆฌ๊ณ ์ค๋ฌด์์ ์์ฃผ ๊ฒช๋ ์ค์์ ์ต์ ํ ์ ๋ต๊น์ง, ์ด์ค๊ธ ๊ฐ๋ฐ์๋ฅผ ์ํ ์๋ฒฝ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํด์.
๋จ์ด: 1,983๊ฐ21๋ถ[๐ค] TypeScript ์ ํธ๋ฆฌํฐ ํ์ ์๋ฒฝ ๊ฐ์ด๋: ์ค์ ํ์ฉ ํจํด
TypeScript ์ ํธ๋ฆฌํฐ ํ์ ์ ํต์ฌ ๊ฐ๋ ๊ณผ ์ค์ ํ์ฉ๋ฒ์ ๊น์ด ์๊ฒ ๋ค๋ค์. Pick, Omit, Partial, Required ๋ฑ ์์ฃผ ์ฐ๋ ์ ํธ๋ฆฌํฐ ํ์ ์ผ๋ก ๋ณต์กํ ํ์ ์ ํจ๊ณผ์ ์ผ๋ก ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์. ํ์ ์คํฌ๋ฆฝํธ ์ฝ๋์ ์ฌ์ฌ์ฉ์ฑ๊ณผ ์์ ์ฑ์ ๋์ด๋ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํด์.
- ๋จ์ด: 1,712๊ฐ20๋ถ
[๐ค] Next.js App Router ๋ฏธ๋ค์จ์ด: ๊ฐ๋ ฅํ ์์ฒญ ์ฒ๋ฆฌ ์ ๋ต๊ณผ ์ค์ ์์
Next.js App Router ํ๊ฒฝ์์ ๋ฏธ๋ค์จ์ด๋ฅผ ํ์ฉํด ์ฌ์ฉ์ ์ธ์ฆ, ๋ฆฌ๋ค์ด๋ ์ , ๊ตญ์ ํ ๋ฑ์ ์์ฒญ ์ฒ๋ฆฌ ๋ก์ง์ ํจ์จ์ ์ผ๋ก ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ค์ ์์ ์ ํจ๊ป ์์ธํ ์์๋ณด์ธ์.
- ๋จ์ด: 1,630๊ฐ19๋ถ
[๐ค] ํ์ ์คํฌ๋ฆฝํธ ์ ๋ค๋ฆญ ์ฌํ: ์ค์ฉ์ ์ธ ํจํด๊ณผ ํํ ์คํด๋ค
ํ์ ์คํฌ๋ฆฝํธ ์ ๋ค๋ฆญ(Generics)์ ๊น์ด ์ดํดํ๊ณ , ์ค๋ฌด์์ ์์ฃผ ์ฌ์ฉ๋๋ ์ ๋ค๋ฆญ ํจํด๊ณผ ํํ ๊ฒช๋ ์คํด๋ค์ ์ค์ ์ฝ๋ ์์์ ํจ๊ป ์ฝ๊ณ ๋ช ํํ๊ฒ ์ค๋ช ํด ๋๋ ค์. ํ์ ์์ ์ฑ๊ณผ ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ์ ๋์ด๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
๋จ์ด: 1,860๊ฐ18๋ถ[๐ค] Next.js Route Handler: App Router์์ ์์ ํ๊ณ ํจ์จ์ ์ธ API ๊ตฌ์ถํ๊ธฐ (์ธ์ฆ, ์๋ฌ ์ฒ๋ฆฌ ํฌํจ)
Next.js App Router์ Route Handler๋ฅผ ์ฌ์ฉํ์ฌ API ์๋ํฌ์ธํธ๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ์์ธํ ์์๋ด์. ์ธ์ฆ, ์๋ฌ ์ฒ๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ์บ์ฑ ์ ๋ต์ ํฌํจํ ์ค์ฉ์ ์ธ ํ์ผ๋ก ์์ ํ๊ณ ํจ์จ์ ์ธ ์๋ฒ๋ฆฌ์ค ํจ์๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์ตํ๋ด์.
- ๋จ์ด: 1,934๊ฐ22๋ถ
[๐ค] Next.js Image ์ปดํฌ๋ํธ ์ต์ ํ: Core Web Vitals ๊ฐ์ ๋ถํฐ ์ค์ ํ์ฉ๊น์ง
Next.js์ Image ์ปดํฌ๋ํธ๋ฅผ ํ์ฉํ์ฌ ์น ์ฑ๋ฅ ํต์ฌ ์งํ์ธ Core Web Vitals๋ฅผ ๊ฐ์ ํ๊ณ , ๋ค์ํ ์ต์ ํ ์ต์ ์ ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ธ๋ฃจ๊ฐ ์์ธํ ์๋ ค๋๋ ค์.
- ๋จ์ด: 2,187๊ฐ25๋ถ
[๐ค] Next.js 14.1+์ ํ์ : Partial Prerendering (PPR) ์๋ฒฝ ๊ฐ์ด๋์ ์ค์ ์ต์ ํ ์ ๋ต
Next.js 14.1๋ถํฐ ๋์ ๋ Partial Prerendering (PPR)์ ํตํด ์ด๊ธฐ ๋ก๋ฉ ์๋๋ฅผ ๊ทน๋ํํ๊ณ ๋์ ์ฝํ ์ธ ๋ฅผ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ ์๊ฒ ๋ค๋ฃจ์ด์. PPR์ ๋์ ์๋ฆฌ๋ถํฐ ์ค์ ํ๋ก์ ํธ ์ ์ฉ ์ ๋ต๊น์ง, ๊ฐ๋ฐ์๋ค์ด ๊ถ๊ธํดํ๋ ๋ชจ๋ ๊ฒ์ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,792๊ฐ19๋ถ
[๐ค] TypeScript ์กฐ๊ฑด๋ถ ํ์ ๊ณผ infer ํค์๋: ๋ณต์กํ ํ์ ๋ ์์ฝ๊ฒ ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ
TypeScript ๊ฐ๋ฐ์์ ๋ง์ฃผํ๋ ๋ณต์กํ ํ์ ์ถ๋ก ๋ฌธ์ , ์กฐ๊ฑด๋ถ ํ์ ๊ณผ infer ํค์๋๋ฅผ ํ์ฉํ๋ฉด ํจ์ฌ ์ฐ์ํ๊ณ ๊ฐ๋ ฅํ๊ฒ ํด๊ฒฐํ ์ ์์ด์. ์ค์ ์์ ์ ํจ๊ป ๊ทธ ํ์ฉ๋ฒ์ ์ฌ๋ ์๊ฒ ๋ค๋ค๋ด ๋๋ค.
- ๋จ์ด: 1,705๊ฐ21๋ถ
[๐ค] JavaScript ์ด๋ฒคํธ ๋ฃจํ(Event Loop) ์์ ์ ๋ณต: ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๋ฐํ์ ๋์ ์๋ฆฌ
JavaScript์ ํต์ฌ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๋ฉ์ปค๋์ฆ์ธ ์ด๋ฒคํธ ๋ฃจํ์ ๋์ ์๋ฆฌ๋ฅผ ์ฌ๋ ์๊ฒ ํํค์ณ ๋ด์. ์ฝ ์คํ, ํ์คํฌ ํ, ๋ง์ดํฌ๋กํ์คํฌ ํ์์ ์ํธ์์ฉ์ ์ดํดํ๊ณ , ์ค๋ฌด์์ ๋ง์ฃผ์น๋ ๋น๋๊ธฐ ์ฝ๋์ ๋์์ ๋ช ํํ ์์ธกํ๋ ๋ฐฉ๋ฒ์ ์๋ ค๋๋ ค์.
- ๋จ์ด: 1,964๊ฐ23๋ถ
[๐ค] Next.js Server & Client Components, ์ค์ ์์ ํ๋ช ํ๊ฒ ์ ํํ๋ ๊ฐ์ด๋
Next.js App Router์์ Server Components์ Client Components ์ค ์ด๋ค ๊ฒ์ ์ฌ์ฉํด์ผ ํ ์ง ๊ณ ๋ฏผ์ด์ ๊ฐ์? ์ด ๊ธ์์ ๋ ์ปดํฌ๋ํธ์ ํต์ฌ ์ฐจ์ด์ , ์ฌ์ฉ ์์ , ๊ทธ๋ฆฌ๊ณ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํ ์ค์ ์ ๋ต์ ๋ธ๋ฃจ๊ฐ ์๋ ค๋๋ฆด๊ฒ์.
- ๋จ์ด: 1,879๊ฐ21๋ถ
[๐ค] TypeScript satisfies ์ฐ์ฐ์: ํ์ ์ถ๋ก ๊ณผ ์์ ์ฑ์ ๋์์ ์ก๋ ๋น๋ฒ
TypeScript์ `satisfies` ์ฐ์ฐ์๋ฅผ ํ์ฉํ์ฌ ํ์ ์ถ๋ก ์ ์ ์ฐ์ฑ์ ์ ์งํ๋ฉด์๋ ์๊ฒฉํ ํ์ ์์ ์ฑ์ ํ๋ณดํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์. ์ค์ฉ์ ์ธ ์์๋ฅผ ํตํด ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๋ ธํ์ฐ๋ฅผ ๊ณต์ ํฉ๋๋ค.
- ๋จ์ด: 1,211๊ฐ15๋ถ
[๐ค] React 19 ์๋ก์ด ๊ธฐ๋ฅ: use ํ , Actions, ๊ทธ๋ฆฌ๊ณ ์ปดํ์ผ๋ฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
React 19์ ํต์ฌ ๋ณ๊ฒฝ ์ฌํญ์ธ use ํ , ์๋ฒ ์ก์ , ๊ทธ๋ฆฌ๊ณ React ์ปดํ์ผ๋ฌ์ ๋์ ๋ฐฐ๊ฒฝ๊ณผ ์ค์ ํ์ฉ ์์๋ฅผ ์ด์ค๊ธ ๊ฐ๋ฐ์ ๋๋์ด์ ๋ง์ถฐ ์์ธํ ์ค๋ช ํฉ๋๋ค. ์ต์ React ์ ๋ฐ์ดํธ๋ฅผ ํตํด ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ๊ณผ ๊ฐ๋ฐ ๊ฒฝํ์ ํฅ์์ํค๋ ๋ฐฉ๋ฒ์ ์์๋ณด์ธ์.
- ๋จ์ด: 1,524๊ฐ16๋ถ
[๐ค] Next.js App Router ์บ์ฑ ์ ๋ต: ๋ฐ์ดํฐ ์ฌ๊ฒ์ฆ (revalidatePath, revalidateTag) ์๋ฒฝ ๊ฐ์ด๋
Next.js 14 App Router์์ ํจ์จ์ ์ธ ๋ฐ์ดํฐ ์บ์ฑ ์ ๋ต๊ณผ revalidatePath, revalidateTag๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ ์ฌ๊ฒ์ฆ ๋ฐฉ๋ฒ์ ์ค๋ฌด ์์์ ํจ๊ป ์์ธํ ์์๋ณด๊ณ ์น ์ฑ๋ฅ์ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.