본문으로 바로가기 본문 바로가기
WIKI 기술 지식 베이스

직원 디렉터리 애플리케이션 만들기

원문 보기 위키 갱신

이 튜토리얼에서는 D1을 사용해 직원 디렉터리(staff directory)를 만드는 방법을 배워 볼게요. 이 애플리케이션은 사용자가 조직의 직원 정보에 접근할 수 있게 해 주고, 관리자는 앱 안에서 바로 새 직원을 추가할 수 있게 해 줘요. 이를 위해 먼저 데이터를 매끄럽게 관리할 D1 데이터베이스를 설정한 다음, HonoX Framework ↗︎와 Cloudflare Pages를 사용해서 애플리케이션을 개발하고 배포해 볼 거예요.

출처: 문서

본문

사전 준비 (Prerequisites)

이 튜토리얼을 진행하기 전에 다음 항목이 준비되어 있는지 확인하세요.

  • Cloudflare 계정 — 아직 없다면 계속하기 전에 가입 ↗︎하세요.
  • 최신 버전의 npm ↗︎이 설치되어 있어야 해요.

지금 바로 셋업을 진행하고 싶지 않다면 GitHub에서 완성된 코드 ↗︎를 확인해 보세요.

1. HonoX 설치하기

이 튜토리얼에서는 풀스택 웹사이트와 Web API를 만드는 메타 프레임워크인 HonoX ↗︎를 사용해서 애플리케이션을 만들 거예요. 프로젝트에서 HonoX를 사용하려면 hono-create 명령을 실행하세요.

시작하려면 다음 명령을 실행하세요.

npm create hono@latest

셋업 과정에서 프로젝트 디렉터리 이름을 입력하고 템플릿을 선택하라는 안내가 나올 거예요. 선택할 때 x-basic 템플릿을 고르세요.

2. HonoX 애플리케이션 초기화하기

프로젝트가 셋업되면 아래와 같이 생성된 파일 목록을 볼 수 있어요. 이는 HonoX 애플리케이션의 전형적인 프로젝트 구조예요.

.
├── app
│   ├── global.d.ts // global type definitions
│   ├── routes
│   │   ├── _404.tsx // not found page
│   │   ├── _error.tsx // error page
│   │   ├── _renderer.tsx // renderer definition
│   │   ├── about
│   │   │   └── [name].tsx // matches `/about/:name`
│   │   └── index.tsx // matches `/`
│   └── server.ts // server entry file
├── package.json
├── tsconfig.json
└── vite.config.ts

이 프로젝트에는 앱 코드·라우트·서버 셋업 디렉터리와 함께, 패키지 관리·TypeScript·Vite를 위한 설정 파일들이 포함돼 있어요.

3. 데이터베이스 만들기

프로젝트용 데이터베이스를 만들려면 D1 데이터베이스 작업을 위한 wrangler d1 명령을 지원하는 Cloudflare CLI 도구인 Wrangler를 사용하세요. 다음 명령으로 staff-directory라는 새 데이터베이스를 만드세요.

npx wrangler d1 create staff-directory

데이터베이스를 만든 다음에는 애플리케이션과 데이터베이스를 연동하기 위해 Wrangler 설정 파일에 바인딩(binding)을 설정해야 해요.

이 바인딩을 통해 애플리케이션이 D1 데이터베이스, KV 네임스페이스, R2 버킷 같은 Cloudflare 리소스와 상호작용할 수 있어요. 이를 설정하려면 프로젝트 루트 디렉터리에 Wrangler 파일을 만들고 기본 셋업 정보를 입력하세요.

{
	"$schema": "./node_modules/wrangler/config-schema.json",
	"name": "staff-directory",
	// Set this to today's date
	"compatibility_date": "2026-10-07"
}
"$schema" = "./node_modules/wrangler/config-schema.json"
name = "staff-directory"
# Set this to today's date
compatibility_date = "2026-10-07"

다음으로 Wrangler 파일에 데이터베이스 바인딩 세부 정보를 추가하세요. 여기에는 애플리케이션 안에서 데이터베이스를 참조하는 데 사용할 바인딩 이름(이 경우 DB)과, 데이터베이스를 만들 때 제공받은 database_name 및 database_id를 지정하는 과정이 포함돼요.

{
	"d1_databases": [
		{
			"binding": "DB",
			"database_name": "staff-directory",
			"database_id": "f495af5f-dd71-4554-9974-97bdda7137b3"
		}
	]
}
[[d1_databases]]
binding = "DB"
database_name = "staff-directory"
database_id = "f495af5f-dd71-4554-9974-97bdda7137b3"

이제 애플리케이션이 D1 데이터베이스에 접근하고 상호작용하도록, 명령줄 또는 코드베이스 안에서 직접 구성했어요.

또한 vite.config.js의 Vite 설정 파일도 조정해야 해요. 로컬 환경에서 Cloudflare 바인딩과 잘 연동되도록 다음 설정을 추가하세요.

import adapter from "@hono/vite-dev-server/cloudflare";

export default defineConfig(({ mode }) => {
	if (mode === "client") {
		return {
			plugins: [client()],
		};
	} else {
		return {
			plugins: [
				honox({
					devServer: {
						adapter,
					},
				}),
				pages(),
			],
		};
	}
});

4. D1과 상호작용하기

D1 데이터베이스와 상호작용하려면 wrangler d1 execute 명령으로 SQL 명령을 직접 실행할 수 있어요.

wrangler d1 execute staff-directory --command "SELECT name FROM sqlite_schema WHERE type ='table'"

위 명령을 사용하면 명령줄에서 직접 쿼리나 작업을 실행할 수 있어요.

최초 데이터 시딩(seeding)이나 배치 처리 같은 작업이 필요하면 명령이 담긴 SQL 파일을 전달할 수 있어요. 이렇게 하려면 프로젝트 루트 디렉터리에 schema.sql 파일을 만들고 SQL 쿼리를 넣으세요.

CREATE TABLE locations (
    location_id INTEGER PRIMARY KEY AUTOINCREMENT,
    location_name VARCHAR(255) NOT NULL
);

CREATE TABLE departments (
    department_id INTEGER PRIMARY KEY AUTOINCREMENT,
    department_name VARCHAR(255) NOT NULL
);

CREATE TABLE employees (
    employee_id INTEGER PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(255) NOT NULL,
    position VARCHAR(255) NOT NULL,
    image_url VARCHAR(255) NOT NULL,
    join_date DATE NOT NULL,
    location_id INTEGER REFERENCES locations(location_id),
    department_id INTEGER REFERENCES departments(department_id)
);

INSERT INTO locations (location_name) VALUES ('London, UK'), ('Paris, France'), ('Berlin, Germany'), ('Lagos, Nigeria'), ('Nairobi, Kenya'), ('Cairo, Egypt'), ('New York, NY'), ('San Francisco, CA'), ('Chicago, IL');

INSERT INTO departments (department_name) VALUES ('Software Engineering'), ('Product Management'), ('Information Technology (IT)'), ('Quality Assurance (QA)'), ('User Experience (UX)/User Interface (UI) Design'), ('Sales and Marketing'), ('Human Resources (HR)'), ('Customer Support'), ('Research and Development (R&D)'), ('Finance and Accounting');

위 쿼리는 Locations, Departments, Employees 세 개의 테이블을 만들 거예요. 이 테이블에 초기 데이터를 채우려면 INSERT INTO 명령을 사용하세요. 스키마 파일을 준비한 다음에는 --file 플래그로 스키마 파일을 지정해서 D1 데이터베이스에 적용할 수 있어요.

wrangler d1 execute staff-directory --file=./schema.sql

위 명령에 --local 플래그를 전달하면 스키마를 로컬에서 실행하고 로컬 디렉터리에 데이터를 시딩할 수 있어요.

5. SQL 구문 만들기

앞선 단계에서 설명한 대로 D1 데이터베이스를 설정하고 Wrangler 파일을 구성했다면, 이제 코드에서 DB 바인딩을 통해 데이터베이스에 접근할 수 있어요. 이를 통해 SQL 구문을 준비하고 실행해서 데이터베이스와 직접 상호작용할 수 있어요. 다음 단계에서는 이 바인딩을 사용해 데이터 조회, 새 레코드 삽입 같은 일반적인 데이터베이스 작업을 수행하는 방법을 배워 볼게요.

데이터베이스에서 데이터 조회하기

export const findAllEmployees = async (db: D1Database) => {
	const query = `
      SELECT employees.*, locations.location_name, departments.department_name
      FROM employees
      JOIN locations ON employees.location_id = locations.location_id
      JOIN departments ON employees.department_id = departments.department_id
      `;
	const { results } = await db.prepare(query).run();
	const employees = results;
	return employees;
};

데이터베이스에 데이터 삽입하기

export const createEmployee = async (db: D1Database, employee: Employee) => {
	const query = `
      INSERT INTO employees (name, position, join_date, image_url, department_id, location_id)
      VALUES (?, ?, ?, ?, ?, ?)`;

	const results = await db
		.prepare(query)
		.bind(
			employee.name,
			employee.position,
			employee.join_date,
			employee.image_url,
			employee.department_id,
			employee.location_id,
		)
		.run();
	const employees = results;
	return employees;
};

애플리케이션에서 사용하는 전체 쿼리 목록은 코드베이스의 db.ts ↗︎ 파일을 참고하세요.

6. UI 개발하기

이 애플리케이션은 렌더링에 hono/jsx를 사용해요. app/routes/_renderer.tsx에서 JSX 렌더링 미들웨어를 사용해 Renderer를 설정할 수 있어요. 이 Renderer가 애플리케이션의 진입점 역할을 해요.

import { jsxRenderer } from 'hono/jsx-renderer'
import { Script } from 'honox/server'

export default jsxRenderer(({ children, title }) => {
  return (
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>{title}</title>
        <Script src="/app/client.ts" async />
      </head>
      <body>{children}</body>
    </html>
  )
})

앞서 정의한 바인딩을 TypeScript 전역 타입 정의가 있는 global.d.ts 파일에 추가해서 애플리케이션 전반에 걸쳐 타입 일관성을 유지하세요.

declare module "hono" {
	interface Env {
		Variables: {};
		Bindings: {
			DB: D1Database;
		};
	}
}

이 애플리케이션은 스타일링에 Tailwind CSS ↗︎를 사용해요. Tailwind CSS를 사용하려면 TailwindCSS 문서 ↗︎를 참고하거나, GitHub에 제공된 단계 ↗︎를 따라 하세요.

직원 목록을 표시하려면 db.ts 파일의 findAllEmployees 함수를 호출해서 routes/index.tsx 파일 안에서 호출하세요. 파일 안에 있는 createRoute() 함수는 GET, POST, PUT, DELETE 같은 다양한 HTTP 메서드를 처리하는 라우트를 정의하는 헬퍼 함수 역할을 해요.

import { css } from 'hono/css'
import { createRoute } from 'honox/factory'
import Counter from '../islands/counter'

const className = css`
  font-family: sans-serif;
`

export default createRoute((c) => {
  const name = c.req.query('name') ?? 'Hono'
  return c.render(
    <div class={className}>
      <h1>Hello, {name}!</h1>
      <Counter />
    </div>,
    { title: name }
  )
})

파일 안의 기존 코드에는 Counter 컴포넌트를 사용하는 플레이스홀더가 있어요. 이 부분을 다음 코드 블록으로 교체하세요.

import { createRoute } from 'honox/factory'
import type { FC } from 'hono/jsx'
import type { Employee } from '../db'
import { findAllEmployees, findAllDepartments, findAllLocations } from '../db'

const EmployeeCard: FC<{ employee: Employee }> = ({ employee }) => {
  const { employee_id, name, image_url, department_name, location_name } = employee;
  return (
    <div className="max-w-sm bg-white border border-gray-200 rounded-lg shadow-md">
      <a href={`/employee/${employee_id}`}>
        <img className="bg-indigo-600 p-4 rounded-t-lg" src={image_url} alt={name} />
        //...
      </a>
    </div>
  );
};

export const GET = createRoute(async (c) => {
  const employees = await findAllEmployees(c.env.DB)
  const locations = await findAllLocations(c.env.DB)
  const departments = await findAllDepartments(c.env.DB)
  return c.render(
    <section className="flex-grow">
      <h1 className="mb-4 text-3xl font-extrabold text-gray-900 dark:text-white md:text-5xl lg:text-6xl mt-12">
        <span className="text-transparent bg-clip-text bg-gradient-to-r to-blue-600 from-sky-400">{`Directory `}</span>
      </h1>
      //...
      </section>
      <section className="flex flex-wrap -mx-4">
        {employees.map((employee) => (
          <div className="w-full sm:w-1/2 md:w-1/3 lg:w-1/4 px-2 mb-4">
            <EmployeeCard employee={employee} />
          </div>
        ))}
      </section>
    </section>
  )
})

이 코드 스니펫은 db.ts 파일에서 findAllEmployees, findAllLocations, findAllDepartments 함수를 가져오는 방법과, c.env.DB 바인딩을 사용해 이 함수들을 호출하는 방법을 보여줘요. 이를 통해 가져온 데이터를 페이지에서 조회하고 표시할 수 있어요.

직원 추가하기

export POST 라우트를 사용해서 /admin 페이지에서 새 직원을 만들 수 있어요.

import { createRoute } from "honox/factory";
import type { Employee } from "../../db";
import { getFormDataValue, getFormDataNumber } from "../../utils/formData";
import { createEmployee } from "../../db";

export const POST = createRoute(async (c) => {
	try {
		const formData = await c.req.formData();
		const imageFile = formData.get("image_file");
		let imageUrl = "";

		// TODO: process image url with R2

		const employeeData: Employee = {
			employee_id: getFormDataValue(formData, "employee_id"),
			name: getFormDataValue(formData, "name"),
			position: getFormDataValue(formData, "position"),
			image_url: imageUrl,
			join_date: getFormDataValue(formData, "join_date"),
			department_id: getFormDataNumber(formData, "department_id"),
			location_id: getFormDataNumber(formData, "location_id"),
			location_name: "",
			department_name: "",
		};

		await createEmployee(c.env.DB, employeeData);
		return c.redirect("/", 303);
	} catch (error) {
		return new Response("Error processing your request", { status: 500 });
	}
});

R2에 이미지 저장하기

새 직원을 만드는 과정에서 업로드된 이미지는 데이터베이스에 추가되기 전에 R2 버킷에 저장될 수 있어요.

R2 버킷에 이미지를 저장하려면:

  1. R2 버킷을 만드세요.
  2. 이미지를 이 버킷에 업로드하세요.
  3. 버킷에서 이미지의 공개 URL을 얻으세요. 이 URL을 데이터베이스에 저장해서 R2 버킷에 저장된 이미지를 연결해요.

버킷을 만들려면 wrangler r2 bucket create 명령을 사용하세요.

wrangler r2 bucket create employee-avatars

버킷이 만들어지면 Wrangler 파일에 R2 버킷 바인딩을 추가하세요.

{
	"r2_buckets": [
		{
			"binding": "MY_BUCKET",
			"bucket_name": "employee-avatars"
		}
	]
}
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "employee-avatars"

R2 바인딩을 global.d.ts 파일에 전달하세요.

declare module "hono" {
	interface Env {
		Variables: {};
		Bindings: {
			DB: D1Database;
			MY_BUCKET: R2Bucket;
		};
	}
}

업로드된 이미지를 R2 버킷에 저장하려면 R2가 제공하는 put() 메서드를 사용할 수 있어요. 이 메서드를 사용하면 이미지 파일을 버킷에 업로드할 수 있어요.

if (imageFile instanceof File) {
	const key = `${new Date().getTime()}-${imageFile.name}`;
	const fileBuffer = await imageFile.arrayBuffer();

	await c.env.MY_BUCKET.put(key, fileBuffer, {
		httpMetadata: {
			contentType: imageFile.type || "application/octet-stream",
		},
	});
	console.log(`File uploaded successfully: ${key}`);
	imageUrl = `https://pub-8d936184779047cc96686a631f318fce.r2.dev/${key}`;
}

전체 코드베이스는 GitHub ↗︎를 참고하세요.

7. HonoX 애플리케이션 배포하기

애플리케이션이 배포 준비가 되면 Wrangler를 사용해 프로젝트를 빌드하고 Cloudflare 네트워크에 배포할 수 있어요. wrangler whoami 명령을 실행해 Cloudflare 계정에 로그인되어 있는지 확인하세요. 로그인하지 않았다면 Wrangler가 API 키를 생성해 자동으로 인증된 요청을 보낼 수 있도록 로그인을 안내할 거예요.

로그인이 완료되면 Wrangler 파일이 아래 코드 블록과 비슷하게 구성되어 있는지 확인하세요.

{
	"$schema": "./node_modules/wrangler/config-schema.json",
	"name": "staff-directory",
	// Set this to today's date
	"compatibility_date": "2026-10-07",
	"r2_buckets": [
		{
			"binding": "MY_BUCKET",
			"bucket_name": "employee-avatars"
		}
	],
	"d1_databases": [
		{
			"binding": "DB",
			"database_name": "staff-directory",
			"database_id": "f495af5f-dd71-4554-9974-97bdda7137b3"
		}
	]
}
"$schema" = "./node_modules/wrangler/config-schema.json"
name = "staff-directory"
# Set this to today's date
compatibility_date = "2026-10-07"

[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "employee-avatars"

[[d1_databases]]
binding = "DB"
database_name = "staff-directory"
database_id = "f495af5f-dd71-4554-9974-97bdda7137b3"

wrangler deploy를 실행해 프로젝트를 Cloudflare에 배포하세요. 배포 후에는 제공된 배포 URL에 접근해서 애플리케이션이 제대로 동작하는지 테스트할 수 있어요. 브라우저에 여러분이 만든 기본 프런트엔드가 표시될 거예요. 데이터베이스에 데이터가 없다면 /admin 페이지로 가서 새 직원을 추가해 보세요. 그러면 홈 페이지에 새 직원이 표시될 거예요.

결론 (Conclusion)

이 튜토리얼에서는 사용자가 조직 내 모든 직원을 조회할 수 있는 직원 디렉터리 애플리케이션을 만들었어요. 전체 소스 코드는 Staff directory 저장소 ↗︎를 참고하세요.

staff directory demo

더 알아보기 (Learn more)