Next.js / Server дээр өгөгдөл татах

Server дээр өгөгдөл татах

Next.js-н App Router-н хамгийн хүчтэй тал бол Server Component дотор шууд өгөгдөл татах чадвар. useEffect дотор fetch дуудах, loading state удирдах шаардлагагүй — async/await ашиглан шууд бичнэ. Энэ хичээлд server дата татах бүх арга замыг судална.

async Server Component

Server Component нь async функц байж болно. Ингэснээр await шууд component дотор ашиглах боломжтой:

tsx
// app/courses/page.tsx
import { getAllCourses } from "@/lib/courses";

export default async function CoursesPage() {
  // Сервер дээр ажиллана — хөтөч рүү нэмэлт JS очдоггүй
  const courses = await getAllCourses();

  return (
    <main className="max-w-6xl mx-auto px-4 py-8">
      <h1 className="text-2xl font-bold text-white mb-6">Сургалтууд</h1>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
        {courses.map((course) => (
          <div
            key={course.slug}
            className="bg-[#0f172a] border border-[#1e293b] rounded-xl p-5"
          >
            <h2 className="text-white font-semibold">{course.title}</h2>
            <p className="text-slate-400 text-sm mt-1">{course.description}</p>
          </div>
        ))}
      </div>
    </main>
  );
}

Файл системээс унших

content/ директорт хадгалсан markdown, JSON файлуудыг Server Component-с шууд унших боломжтой:

typescript
// lib/courses.ts
import { readFile, readdir } from "fs/promises";
import path from "path";

export interface Lesson {
  slug: string;
  title: string;
  order: number;
}

export interface Course {
  slug: string;
  title: string;
  description: string;
  color: string;
  isFree: boolean;
  lessons: Lesson[];
}

export async function getAllCourses(): Promise<Course[]> {
  const coursesDir = path.join(process.cwd(), "content/courses");
  const slugs = await readdir(coursesDir);

  const courses = await Promise.all(
    slugs.map(async (slug) => {
      const jsonPath = path.join(coursesDir, slug, "course.json");
      const raw = await readFile(jsonPath, "utf-8");
      return JSON.parse(raw) as Course;
    }),
  );

  return courses.sort((a, b) => a.slug.localeCompare(b.slug));
}

export async function getCourse(slug: string): Promise<Course | null> {
  try {
    const jsonPath = path.join(
      process.cwd(),
      "content/courses",
      slug,
      "course.json",
    );
    const raw = await readFile(jsonPath, "utf-8");
    return JSON.parse(raw) as Course;
  } catch {
    return null; // Файл олдоогүй
  }
}

export async function getLessonContent(
  courseSlug: string,
  lessonSlug: string,
): Promise<string | null> {
  try {
    const mdPath = path.join(
      process.cwd(),
      "content/courses",
      courseSlug,
      "lessons",
      lessonSlug,
      "lesson.md",
    );
    return await readFile(mdPath, "utf-8");
  } catch {
    return null;
  }
}

fetch ашиглан гадаад API-с татах

Next.js-н fetch нь Web стандарт fetch-г өргөтгөж кэшлэх боломж нэмсэн:

tsx
// app/page.tsx
interface GithubRepo {
  name: string;
  stargazers_count: number;
  html_url: string;
}

export default async function HomePage() {
  // fetch — сервер дээр ажиллана
  const res = await fetch("https://api.github.com/users/vercel/repos", {
    next: { revalidate: 3600 }, // 1 цагт нэг удаа шинэчлэнэ
  });

  if (!res.ok) {
    throw new Error("GitHub API-с өгөгдөл татахад алдаа гарлаа");
  }

  const repos: GithubRepo[] = await res.json();

  return (
    <ul>
      {repos.slice(0, 5).map((repo) => (
        <li key={repo.name}>
          <a href={repo.html_url} className="text-indigo-400">
            {repo.name}
          </a>
          <span className="text-slate-400 ml-2">
            ⭐ {repo.stargazers_count}
          </span>
        </li>
      ))}
    </ul>
  );
}

Supabase-с шууд татах

Server Component дотор Supabase client-г шууд ашиглаж болно — хамгийн энгийн, аюулгүй арга:

tsx
// app/profile/page.tsx
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";

export default async function ProfilePage() {
  const supabase = await createClient();

  // Хэрэглэгчийн мэдээлэл авах
  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    redirect("/login");
  }

  // Профайл ба явц нэгэн зэрэг татах
  const [{ data: profile }, { data: progress }] = await Promise.all([
    supabase
      .from("profiles")
      .select("username, xp, streak")
      .eq("id", user.id)
      .single(),
    supabase
      .from("lesson_progress")
      .select("course_slug, lesson_slug, completed_at")
      .eq("user_id", user.id)
      .order("completed_at", { ascending: false })
      .limit(5),
  ]);

  return (
    <main className="max-w-2xl mx-auto px-4 py-8">
      <h1 className="text-2xl font-bold text-white">
        Сайн уу, {profile?.username}!
      </h1>
      <p className="text-slate-400 mt-1">
        XP: {profile?.xp} · Streak: {profile?.streak} өдөр
      </p>

      <h2 className="text-white font-semibold mt-8 mb-3">
        Сүүлийн үйл ажиллагаа
      </h2>
      <ul className="space-y-2">
        {progress?.map((p) => (
          <li
            key={`${p.course_slug}-${p.lesson_slug}`}
            className="text-slate-400 text-sm"
          >
            {p.course_slug} / {p.lesson_slug}
          </li>
        ))}
      </ul>
    </main>
  );
}

Promise.all — Зэрэгцээ татах

Хэд хэдэн өгөгдлийг нэг нэгээр татвал удаашрана. Promise.all ашиглан зэрэгцээ татахад нийт хугацаа богиносно:

tsx
// ❌ Удаан — дараалал: 200мс + 150мс + 100мс = 450мс
const course = await getCourse(courseSlug);
const lessons = await getLessons(courseSlug);
const progress = await getProgress(userId, courseSlug);

// ✅ Хурдан — зэрэгцэн: max(200, 150, 100) = 200мс
const [course, lessons, progress] = await Promise.all([
  getCourse(courseSlug),
  getLessons(courseSlug),
  getProgress(userId, courseSlug),
]);

Дараагийн хичээлд:

Client дээр өгөгдөл татах аргыг судална. useState + useEffect ашиглах сонгодог арга болон хөтчөөс API route дуудах загварыг жишээгээр ойлгоно.