1. better-sqlite3 설치
npm install better-sqlite3
2. initdb.js 설정
// 라이브러리 불러오기
const sql = require("better-sqlite3");
const db = sql("meals.db");
const dummyMeals = [ ...
];
// 테이블 설정
db.prepare(
`
CREATE TABLE IF NOT EXISTS meals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
image TEXT NOT NULL,
summary TEXT NOT NULL,
instructions TEXT NOT NULL,
creator TEXT NOT NULL,
creator_email TEXT NOT NULL
)
`
).run();
// 데이터베이스에 여러 데이터를 입력하기 위함
async function initData() {
const stmt = db.prepare(`
INSERT INTO meals VALUES (
null,
@slug,
@title,
@image,
@summary,
@instructions,
@creator,
@creator_email
)
`);
// 더미 데이터를 데이터베이스에 입력
for (const meal of dummyMeals) {
stmt.run(meal);
}
}
initData();
3. initdb.js 실행
node initdb.js
initdb.js
를 실행하면 meals.db
가 생성되고, 이는 사용할 SQL Lite 데이터베이스이다.
React에서 데이터를 불러올 때 처럼 useEffect
훅을 사용하여 그 안에 fetch 함수를 통해 백엔드에 요청해 데이터를 가져올 수도 있지만, NextJS는 이미 백엔드를 가지고 있음으로 백엔드를 분리시킬 필요가 없다.
NextJS는 기본적으로 모든 컴포넌트들이 서버에서만 실행되는 서버 컴포넌트이다. 따라서 fetch 요청을 하지 않고도 바로 데이터베이스로 갈 수 있다.
// lib/meals.js
import sql from "better-sqlite3";
// 데이터베이스 연결
const db = sql("meals.db");
export const getMeals = async () => {
await new Promise((resolve) => setTimeout(resolve, 2000)); // 지연
return db.prepare("SELECT * FROM meals").all(); // 데이터 가져오기
};
run()
: 데이터를 바꿀 때 사용all()
: 데이터를 모두 가져올 때 사용get()
: 하나의 데이터를 가져올 때 사용서버 컴포넌트에서는 async-await
사용 가능
NextJS는 기본적으로 서버 컴포넌트이기 때문에 React와 달리
async
함수로 바꿀 수 있고, 만약 promise를 사용한 코드가 있다면await
를 사용할 수 있다.const MealsPage = async () => { const meals = await getMeals(); return ( <> // 생략 <MealsGrid meals={meals} /> </> ); };
loading.js
의 경우 page.js
나 layout.js
와 같이 특별히 지정된 파일 이름으로, 중첩된 페이지가 데이터를 불러올 때 대체로 보여진다.
import { Suspense } from "react";
import { getMeals } from "@/lib/meals";
// Meals 컴포넌트 생성 (여기서 meals 데이터를 가져오고 MealsGrid 컴포넌트를 반환 함)
const Meals = async () => {
const meals = await getMeals();
return <MealsGrid meals={meals} />;
};
const MealsPage = () => {
return (
<>
// 생략
<main className={classes.main}>
// Suspense 컴포넌트로 데이터를 불러오는 Meals 컴포넌트를 wrapping
<Suspense
fallback={<p className={classes.loading}>Fetching meals...</p>}
>
// 여기서 Meals 컴포넌트를 사용
<Meals />
</Suspense>
</main>
</>
);
};
데이터를 가져오는 부분을 Meals
컴포넌트로 분리하여 react에 내장된 Suspense
컴포넌트로 wrapping 할 수 있다.
이때 Suspense
는 react에서 제공하는 컴포넌트로 일부 데이터를 불러올 때까지 로딩 상태로 처리하고, 대체 콘텐츠를 표시할 수 있다. Suspense
의 fallback
속성을 이용하여 데이터를 불러올 동안 대체할 콘텐츠를 담아주면, Meals
컴포넌트가 데이터를 불러올 동안 fallback
에 담은 내용을 표시한다.
이처럼 데이터를 가져오는 부분을 다른 컴포넌트로 분리하고 Suspense
를 사용하면 로딩이 불필요한 부분(헤더와 같은)은 부분적으로 미리 랜더링하고, 로딩이 필요한 부분만 이후에 랜더링하여 더 나은 사용자 경험을 만들 수 있다.
error.js
파일을 통해 오류를 처리할 수 있다. error.js
는 특별히 지정된 파일 이름 중 하나로, 같은 폴더에 있거나 중첩된 페이지나 레이아웃에서 발생한 오류를 처리한다. root
레벨에 둘 경우 애플리케이션 어느 페이지에서 발생한 오류도 처리할 수 있다.
// 클라이언트 컴포넌트여야 함
"use client";
const Error = () => {
return (
<main className="error">
<h1>An error occurred!</h1>
<p>Failed to fetch meals data. Please try again later.</p>
</main>
);
};
export default Error;
단, error.js
에 지정된 error
컴포넌트는 반드시 클라이언트 컴포넌트여야 한다. NextJS는 기본적으로 클라이언트 측에서 발생하는 오류를 포함한, 해당 컴포넌트의 모든 오류를 잡을 수 있도록 보장하기 때문이다.
export default function NotFound() {
return (
<main className="not-found">
<h1>Meal Not found</h1>
<p>Unfortunately, we could not find the requested page or meal data.</p>
</main>
);
}
not-found.js
파일을 통해 Not Found 오류를 처리할 수 있다. not-found.js
역시 특별히 지정된 파일 이름 중 하나로, 같은 폴더에 있거나 중첩된 페이지나 레이아웃에서 발생한 Not Found 오류를 처리한다. root
레벨에 둘 경우 애플리케이션 어느 페이지에서 발생한 Not Found 오류도 처리할 수 있다.
이와 같은 방법으로 잘못된 url로 갔을 때 Not Found 페이지가 보여지도록 할 수 있다.
// lib/meals.js
import sql from "better-sqlite3";
const db = sql("meals.db");
export const getMeal = (slug) => {
return db.prepare("SELECT * FROM meals WHERE slug = ?").get(slug);
};
세부내용을 가져오기 위해서는 해당 정보를 식별할 수 있는 slug가 있어야 한다. 해당 slug와 동일한 데이터를 가져와야 하나, SQL 인젝션에 노출될 수 있음을 방지하기 위해 ?
를 플레이스홀더로 사용하고, get()
메서드를 사용해 해당 플레이스홀더에 들어가야 하는 값을 전달한다.
// app/meals/[mealSulg]/page.js
import Image from "next/image";
import { getMeal } from "@/lib/meals";
import classes from "./page.module.css";
const MealDetailsPage = ({ params }) => {
// meal 데이터 가져오기
const meal = getMeal(params.mealSlug);
meal.instructions = meal.instructions.replace(/\n/g, "<br />");
return (
<>
<header className={classes.header}>
<div className={classes.image}>
<Image src={meal.image} alt={meal.title} fill />
</div>
<div className={classes.headerText}>
<h1>{meal.title}</h1>
<p className={classes.creator}>
by <a href={`mailto:${meal.creator_email}`}>{meal.creator}</a>
</p>
<p className={classes.summary}>{meal.summary}</p>
</div>
</header>
<main>
<p
className={classes.instructions}
dangerouslySetInnerHTML={{ __html: meal.instructions }}
></p>
</main>
</>
);
};
export default MealDetailsPage;
getMeal()
를 통해 meal 데이터를 가져올 때, 필요한 것이 slug이다. 이때 slug는 이 페이지에서 받는 특별한 props인 params
props에서 확인할 수 있다. params.mealSulg
를 통해 해당 slug를 getMeal()
에 전달하면 meal 데이터를 얻을 수 있다.
// app/meals/[mealSulg]/page.js
import { notFound } from "next/navigation";
if (!meal) {
notFound();
}
meal 데이터를 찾을 수 없는 경우 not-found 페이지를 보여주는 것이 좋다. 이럴 때, NextJS에서 제공되는 특별한 함수인 notFound
를 활용할 수 있다. notFound
는 페이지를 찾을 수 없을 때 해당 컴포넌트의 실행을 멈추고, 제일 가까운 not-found.js
나 error.js
를 보여준다.