Syw.Frontend

๐Ÿ“˜ Next.js ๊ธฐ์ดˆ ๊ฐ•์ขŒ

1๋‹จ๊ณ„. Next.js ์ž…๋ฌธ (๊ฐœ๋… & ํ™œ์šฉ ๊ธฐ์ดˆ)

1-8. ๊ฐ„๋‹จ ๋ธ”๋กœ๊ทธ ๋งŒ๋“ค๊ธฐ (๋ชฉ๋ก + ์ƒ์„ธ + ์Šคํƒ€์ผ๋ง + ๋ฐฐํฌ)

1. ๋ชฉํ‘œ

  • /blog : ๊ธ€ ๋ชฉ๋ก ํŽ˜์ด์ง€
  • /blog/[id] : ๊ธ€ ์ƒ์„ธ ํŽ˜์ด์ง€
  • SCSS ๋ชจ๋“ˆ๋กœ ์Šคํƒ€์ผ๋ง
  • ๋ฐฐํฌ ํ›„ ์‹ค์ œ ๋ธ”๋กœ๊ทธ์ฒ˜๋Ÿผ ์ ‘๊ทผ ๊ฐ€๋Šฅ

๋ฐ์ดํ„ฐ๋Š” ์‹ค์Šต์ด๋ฏ€๋กœ JSONPlaceholder API(https://jsonplaceholder.typicode.com/posts)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.


2. ๋ธ”๋กœ๊ทธ ๋ชฉ๋ก ํŽ˜์ด์ง€

src/app/blog/page.tsx

import Link from "next/link";
import styles from "../../styles/blog.module.scss";

type Post = {
  userId: number;
  id: number;
  title: string;
  body: string;
};

export default async function BlogPage() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
    cache: "no-store",
  });
  const posts: Post[] = await res.json();

  return (
    <section className={styles.blogList}>
      <h1>๋ธ”๋กœ๊ทธ ๊ธ€ ๋ชฉ๋ก</h1>
      <ul>
        {posts.slice(0, 10).map((post) => (
          <li key={post.id}>
            <Link href={`/blog/${post.id}`}>
              <h2>{post.title}</h2>
              <p>{post.body.slice(0, 80)}...</p>
            </Link>
          </li>
        ))}
      </ul>
    </section>
  );
}

3. ๋ธ”๋กœ๊ทธ ์ƒ์„ธ ํŽ˜์ด์ง€

src/app/blog/[id]/page.tsx

import styles from "../../../styles/blog.module.scss";

type Post = {
  userId: number;
  id: number;
  title: string;
  body: string;
};

export default async function BlogDetailPage({
  params,
}: {
  params: { id: string };
}) {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`,
    { cache: "no-store" }
  );
  if (!res.ok) {
    return <p>ํ•ด๋‹น ๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.</p>;
  }

  const post: Post = await res.json();

  return (
    <article className={styles.blogDetail}>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

4. ์Šคํƒ€์ผ๋ง (SCSS)

src/styles/blog.module.scss

.blogList {
  max-width: 720px;
  margin: 2rem auto;
  padding: 1rem;

  h1 {
    font-size: 2rem;
    margin-bottom: 1.5rem;
  }

  ul {
    list-style: none;
    padding: 0;
    display: grid;
    gap: 1.2rem;
  }

  li {
    border: 1px solid #eee;
    border-radius: 12px;
    padding: 1rem;
    background: #fff;
    transition: box-shadow 0.2s;

    &:hover {
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
    }

    h2 {
      font-size: 1.25rem;
      margin: 0 0 0.5rem;
    }

    p {
      color: #555;
      font-size: 0.95rem;
      margin: 0;
    }
  }
}

.blogDetail {
  max-width: 720px;
  margin: 2rem auto;
  padding: 1.5rem;
  background: #fff;
  border-radius: 12px;
  border: 1px solid #eee;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.05);

  h1 {
    font-size: 2rem;
    margin-bottom: 1rem;
  }

  p {
    line-height: 1.7;
    color: #333;
  }
}

5. ๋„ค๋น„๊ฒŒ์ด์…˜์— ๋ธ”๋กœ๊ทธ ์ถ”๊ฐ€

src/app/layout.tsx โ†’ <nav> ์•ˆ์— ์ถ”๊ฐ€:

<Link href="/blog">๋ธ”๋กœ๊ทธ</Link>

6. ์˜ค๋Š˜์˜ ์ •๋ฆฌ

  • /blog : ๊ธ€ ๋ชฉ๋ก โ†’ JSON API ์—ฐ๋™
  • /blog/[id] : ๊ธ€ ์ƒ์„ธ โ†’ ๋™์  ๋ผ์šฐํŒ…
  • SCSS๋กœ ๋ธ”๋กœ๊ทธ ์Šคํƒ€์ผ๋ง
  • Vercel ๋ฐฐํฌ ์‹œ https://ํ”„๋กœ์ ํŠธ์ฃผ์†Œ.vercel.app/blog ๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅ

๐Ÿ’ฌ ๋Œ“๊ธ€

    โ€ป ๋กœ๊ทธ์ธ ํ›„ ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.