풀스택

Next.js와 PrismaORM, 그리고 AG Grid로 풀스택 데이터 그리드 앱 만들기 1편

2026. 03. 24. 화요일 오후 10시 42분

들어가기 앞서...

웹 애플리케이션을 개발하는 방법은 정말 다양합니다. 그 중에서도 제가 생각하기에 가장 간단하고 쉬운 기술 스택은 Next.js와 Prisma ORM이라고 생각합니다.

  • Next.js: React 기반의 풀스택 프레임워크
  • Prisma ORM: TypeScript 생태계에서 DB 스키마를 정의하고 타입 안정성이 보장된 쿼리를 작성할 수 있도록 돕는 라이브러리

이번에는 Next.js와 Prisma ORM, 그리고 AG Grid를 사용해서 풀 스택 데이터 그리드 앱을 만들어보겠습니다.
Next와 Prisma를 사용해본 적 없더라도 따라해볼 수 있도록 가이드해드리니, 같이 해보시면 좋을 것 같습니다.

Next.js 설치 및 구성

설치

  • 먼저 아래의 명령어를 터미널에서 실행하여 Next.js 프로젝트를 생성합니다.
1# 보일러 플레이트를 통해 next 앱 생성
2pnpm create next-app@16.2.1 grid-app --yes
3
4# 생성한 프로젝트로 이동
5cd grid-app
6
7# next 앱 실행
8pnpm dev
9

Next.js 간단 설명

Next.js는 파일 시스템 기반의 라우팅을 제공합니다.

Next.js는 파일시스템 기반의 라우팅을 제공합니다. 설명이 좀 거창한데, 단순히 말하면 폴더 구조를 통해 웹페이지의 경로를 설정한다 라고 생각하시면 됩니다.
그럼 그걸 어디서 하냐? 라고 하면, app 폴더에서 합니다.

그래서 app/page.tsx가 랜딩페이지라고 생각하시면 됩니다. page.tsx의 내용을 아래와 같이 수정해보겠습니다.

1export default function Home() {
2  return (
3    <div>
4      <h3>Hello World</h3>
5    </div>
6  );
7}
8

그럼 새 페이지를 만들려면 어떻게 하냐? 라고 하면, 폴더를 만들고 그 안에 page.tsx를 만드시면 끝입니다.
개인적으로 라우팅 설정이 정말 간편해서 Next.js 프레임워크가 참 좋습니다.

page.tsx와 layout.tsx

그런데 app 폴더를 살펴보시면 layout.tsx라는 파일도 있습니다.
이 파일의 역할은 이름처럼 레이아웃을 잡는데 사용합니다.
하위 폴더의 page.tsx 및 layout.tsx는 상위 폴더의 layout.tsx로 감싸진다고 생각하시면 됩니다. 레이아웃에서 공통 UI를 작성하면, 해당 폴더의 모든 하위 폴더에 적용되기 때문에 생산성 측면에서 유리합니다. layout.tsx 파일을 아래와 같이 수정해보겠습니다.

1import type { Metadata } from 'next';
2import { Geist, Geist_Mono } from 'next/font/google';
3import './globals.css';
4
5const geistSans = Geist({ variable: '--font-geist-sans', subsets: ['latin'] });
6const geistMono = Geist_Mono({ variable: '--font-geist-mono', subsets: ['latin'] });
7
8export const metadata: Metadata = { title: 'Grid App', description: 'Next + Prisma + Grid' };
9type RootLayoutType = Readonly<{ children: React.ReactNode }>;
10
11export default function RootLayout({ children }: RootLayoutType) {
12  return (
13    <html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
14      <body className="min-h-full flex flex-col">
15        <header className="p-5 bg-gray-100">
16          <h1 className="text-lg font-bold">Grid App</h1>
17        </header>
18
19        <main className="p-5">{children}</main>
20      </body>
21    </html>
22  );
23}
24

page.tsx는 레이아웃의 children으로 전달됩니다. 따라서 위와 같이 작성하면 페이지 컴포넌트는 항상 main 태그 안에 들어가게 됩니다.
하위 페이지를 만들더라도 main 안에 들어가게 되니, 공통 UI가 항상 적용되어 코드 재사용성을 높일 수 있습니다.

정리하면, Next.js는 파일 시스템 기반의 라우팅을 제공하고, page.tsx로 페이지를, layout.tsx로 하위 레이아웃 및 페이지를 감싸는 wrapper 컴포넌트를 구현할 수 있다 라고 이해하고 넘어가시면 됩니다.

이어서, Next.js의 백엔드에 대해 알아보겠습니다.

서버 컴포넌트와 서버 액션

Next.js는 서버 컴포넌트와 서버 액션을 통해 백엔드를 구현합니다. 먼저 서버 컴포넌트부터 알아보겠습니다.

서버 컴포넌트

서버 컴포넌트의 특징을 정리하면, 아래와 같습니다.

  • 리액트 컴포넌트처럼 함수형으로 작성한다.
  • 비동기 함수로 만들 수 있다.
  • 브라우저가 아닌 서버 사이드(Node)에서 실행된다. 그래서 비즈니스 로직을 직접 실행할 수 있다.
  • useState, useEffect 등의 클라이언트 전용 훅을 사용할 수 없다.

서버에서 실행되고 비동기로 작성할 수 있는 대신, useState, useEffect와 같은 클라이언트 전용 훅을 사용할 수 없습니다. 그래서 서버 컴포넌트는 데이터를 조회하고 하위 컴포넌트에 내려주는 역할을 수행합니다.

앞서 구현한 코드를 수정해보겠습니다.
먼저, 데이터를 가져오는 코드를 작성합니다. 프로젝트 루트 경로에 server/service/get-orders.ts를 생성합니다.
지금은 더미 코드를 작성하겠습니다.

1// server/service/get-orders.ts
2
3/**
4 * 주문 정보를 가져오는 함수
5 */
6export async function getOrders() {
7  return [
8    { id: 1, price: 1000, qty: 5 },
9    { id: 2, price: 2000, qty: 3 },
10    { id: 3, price: 3000, qty: 7 },
11  ];
12}
13

서버 사이드 코드라고 해서 반드시 server 폴더 밑에 작성해야 하는 것은 아닙니다만, 저는 구분을 위해서 서버 컴포넌트를 제외한 서버사이드 코드는 server 폴더 밑에 작성하고 있습니다. 정리를 위해서도, 실수를 방지하기 위해서도 폴더로 구분하는 것이 권장됩니다.
단, 서버 컴포넌트는 반드시 app 폴더 밑에 있어야 합니다!

page.tsx는 아래와 같이 수정합니다.

1// app/page.tsx
2import { getOrders } from '@/server/service/get-orders';
3import { OrderGrid } from './order-grid';
4
5export default async function Home() {
6  // 서버 컴포넌트이므로 비즈니스 로직 직접 호출 가능.
7  const orders = await getOrders();
8
9  return (
10    <div>
11      <OrderGrid orders={orders} />
12    </div>
13  );
14}
15

app 폴더 밑에 order-grid.tsx를 만들고 아래와 같이 작성합니다.

1'use client';
2
3import { getOrders } from '@/server/service/get-orders';
4
5export interface OrderGridProps {
6  orders: Awaited<ReturnType<typeof getOrders>>;
7}
8
9export function OrderGrid({ orders }: OrderGridProps) {
10  return (
11    <div>
12      <ul>
13        {orders.map((o) => (
14          <li key={o.id}>
15            {o.id}, {o.price}, {o.qty}
16          </li>
17        ))}
18      </ul>
19    </div>
20  );
21}
22
  • 서버 컴포넌트에서 내려준 데이터를 그리기 위한 컴포넌트입니다.
  • 아직 useState나 useEffect같은 클라이언트 훅을 호출하고 있지는 않습니다만, 추후에 호출할 것 이므로 상단에 'use client' 지시어를 추가했습니다.
  • Next는 지시어가 없는 컴포넌트를 서버 컴포넌트로 간주합니다. 따라서 클라이언트 전용 훅을 호출해야 하는 컴포넌트라면 'use client' 지시어를 추가해주세요.

여기까지 작업하셨다면 아래와 같은 기능을 구현한 것 입니다.

  • 서버 컴포넌트를 통해 비동기 함수를 호출하여 데이터 조회(아직 데이터 조회 기능은 더미임)
  • 서버 컴포넌트에서 클라이언트 컴포넌트로 조회된 데이터 전달(props)
  • 클라이언트 컴포넌트에서 전달받은 데이터 랜더링(OrderGrid)

서버 액션

Server Action은 클라이언트에서 서버 사이드에서 실행되는 함수를 호출할 수 있도록 해주는 Next의 기능으로, Next 14 버전부터 정식 제공되었습니다.
서버사이드에서 실행되는 관계로, 비즈니스 로직을 이 곳에서 작성해도 됩니다. 사용법도 정말 간단한데, 평범한 자바스크립트 비동기 함수를 작성하듯이 쓰면 됩니다. 대신, 파일 최상단에 'use server' 한 줄만 적어주면 됩니다.

아니 어떻게 클라이언트에서 서버에서 실행되는 비동기 함수를 호출할 수 있지? 라고 의아해 하실 수 있습니다. 개발자가 사용할때는 단순히 함수 호출하는걸로 보이지만, 내부적으로는 상황에 따라 두가지 방법으로 처리됩니다.

  • JavaScript가 없는 경우, HTML의 <form action="...">으로 동작합니다.
    • JS가 로드되지 않았거나, 비활성화 된 환경(ex: bot, 크롤러, SEO 및 인덱싱 )에서도 기능이 동작하도록 해줍니다.
  • JavaScript가 있는 경우, Next.js가 FormSubmit 이벤트를 가로채서 따로 처리합니다.
    • 비동기로 fetch 요청을 보내서 처리합니다. 덕분에 브라우저 전체 새로고침 없이 서버에 Post 요청을 보낼 수 있습니다.

Next는 풀스택 프레임워크이므로 Rest API를 만들 수 있도록 지원합니다. Route Handler를 사용하면 됩니다. 다만 이번 포스트에선 학습을 목적으로 Rest API 대신 Server Action을 사용해보려 합니다.

코드를 작성해보겠습니다. server/service/create-order.ts 파일을 생성해주세요.

1export interface CreateOrderInput {
2  id: number;
3  price: number;
4  qty: number;
5}
6
7// 주문 정보를 생성하는 비즈니스 로직(더미)
8export async function createOrder(input: CreateOrderInput) {
9  return input;
10}
11

그리고 server/action/create-order-action.ts를 만들어주세요.

1'use server';
2
3import { createOrder, CreateOrderInput } from '../service/create-order';
4
5export async function createOrderAction(input: CreateOrderInput) {
6  return createOrder(input);
7}
8

첫 문장을 보시면 'use server' 지시어를 확인하실 수 있습니다. 그리고 export하는 함수는 비동기 함수입니다.
이 조건이 갖춰지면 next는 해당 함수(createOrderAction)를 서버 액션으로 간주합니다.

이제 서버 액션을 클라이언트에서 호출해보겠습니다.
order-grid.tsx를 아래와 같이 수정해주세요.

1'use client';
2
3import { createOrderAction } from '@/server/action/create-order-action';
4import { getOrders } from '@/server/service/get-orders';
5
6export interface OrderGridProps {
7  orders: Awaited<ReturnType<typeof getOrders>>;
8}
9
10export function OrderGrid({ orders }: OrderGridProps) {
11  return (
12    <div>
13      {/* 서버 컴포넌트에서 내려준 orders를 랜더링 */}
14      <ul>
15        {orders.map((o) => (
16          <li key={o.id}>
17            {o.id}, {o.price}, {o.qty}
18          </li>
19        ))}
20      </ul>
21
22      <button
23        className="border-2 p-3"
24        onClick={async () => {
25          const newOrder = await createOrderAction({
26            id: 1,
27            price: 5000,
28            qty: 3,
29          });
30
31          alert(JSON.stringify(newOrder));
32        }}
33      >
34        Create Order
35      </button>
36    </div>
37  );
38}
39
  • 마치 자바스크립트 함수를 호출하는 것 처럼 createOrderAction을 호출하고 있습니다만,
    • 실제로는 상황에 따라 Form Submit을 하거나 fetch 함수가 호출되어 서버에 요청을 전송합니다.
  • Create Order 버튼을 눌러보면, 브라우저에 alert창이 나타나는데, 내용을 보면 서버에서 생성한 order 정보가 출력되는 것을 볼 수 있습니다.
  • 정말 간단하게 함수 정의하는 것 처럼 서버 API를 만들고 호출할 수 있는데다, 타입스크립트를 사용한다면 타입 추론까지 지원되므로, 서버 액션이 반환한 값에 대한 타입 체킹까지 제공되니 더 안전한 풀스택 개발이 가능합니다.

서버액션은 자바스크립트가 없는 경우 Form Submit을 한다고 했지만, 이는 <form action={serverAction}>형태로 사용할때만 유효합니다. 위와 같이 자바스크립트에서 직접 서버 액션을 호출하는 경우엔 JS 없이 동작하지 않습니다.

마치며...

여기까지 해서 Next.js의 라우팅 구조와 layout, page 파일에 대해 알아보았습니다.
또한 서버 컴포넌트를 통해 데이터를 조회하고 서버 액션을 통해 클라이언트에서 서버 사이드의 비즈니스 로직을 실행하는 방법까지 알아보았습니다. 다음 포스트에선 Prisma ORM을 설치하고 앞서 만든 서버사이드의 더미 코드를 구현해보려고 합니다.

해당 포스트에서 다룬 전체 소스코드는 github을 통해 제공됩니다.

긴 글 읽어주셔서 감사합니다!