[NextJS] SQLite 데이터베이스 설정, 데이터 불러오기, 오류 처리하기

나윤빈·2024년 6월 28일
2

Next.js

목록 보기
5/8
post-thumbnail

📚 SQLite 데이터베이스 설정하기

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 데이터베이스이다.

📚 NextJS 데이터 불러오기

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.jslayout.js 와 같이 특별히 지정된 파일 이름으로, 중첩된 페이지가 데이터를 불러올 때 대체로 보여진다.

📚 Suspense & Streamed Response를 이용한세분화 로딩 상태 관리하기

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에서 제공하는 컴포넌트로 일부 데이터를 불러올 때까지 로딩 상태로 처리하고, 대체 콘텐츠를 표시할 수 있다. Suspensefallback 속성을 이용하여 데이터를 불러올 동안 대체할 콘텐츠를 담아주면, 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는 기본적으로 클라이언트 측에서 발생하는 오류를 포함한, 해당 컴포넌트의 모든 오류를 잡을 수 있도록 보장하기 때문이다.

📚 Not Found 처리하기

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 데이터를 얻을 수 있다.

📚 notFound 활용하기

// app/meals/[mealSulg]/page.js
import { notFound } from "next/navigation";

if (!meal) {
  notFound();
}

meal 데이터를 찾을 수 없는 경우 not-found 페이지를 보여주는 것이 좋다. 이럴 때, NextJS에서 제공되는 특별한 함수인 notFound를 활용할 수 있다. notFound는 페이지를 찾을 수 없을 때 해당 컴포넌트의 실행을 멈추고, 제일 가까운 not-found.jserror.js를 보여준다.

참고
Next.js & React - 완벽 정복 가이드

profile
프론트엔드 개발자를 꿈꾸는

0개의 댓글