CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
nickname VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, nickname, email)
VALUES
('John Doe', 'johnd', '[email protected]'),
('Jane Smith', 'janes', '[email protected]'),
('Alice Brown', 'aliceb', '[email protected]'),
('Bob Johnson', 'bobj', '[email protected]');
npm install mysql2
npm install --save-dev @types/mysql
registerService
함수: Next.js가 개발 모드에서 API 경로를 재구축할 때, 데이터베이스 연결 풀을 글로벌 캐시에 저장하여 중복 생성되지 않도록 관리합니다. 이로 인해 개발 환경에서 재연결을 방지할 수 있습니다.executeQuery
함수: SQL 쿼리를 실행하고, 결과를 반환하는 함수입니다. 쿼리 실행 중 에러가 발생하면 콘솔에 출력하고, 에러 처리 후 Promise를 반환합니다.import { Pool, createPool } from 'mysql2';
/**
* check, globalObject, registerService
* Next.js는 개발 모드에서 API 경로를 지속적으로 재구축하는데, initFn()의 경로를 전역으로 지정하여 변경되지 않도록 합니다.
*/
function check(it: false | (Window & typeof globalThis) | typeof globalThis) {
return it && it.Math === Math && it;
}
const globalObject =
check(typeof window === 'object' && window) ||
check(typeof self === 'object' && self) ||
check(typeof global === 'object' && global) ||
(() => {
return this;
})() ||
Function('return this')();
const registerService = (name: string, initFn: any) => {
if (process.env.NODE_ENV === 'development') {
if (!(name in globalObject)) {
globalObject[name] = initFn();
}
return globalObject[name];
}
return initFn();
};
export let db: Pool;
try {
// "db"라는 이름으로 등록된 풀을 글로벌 캐시 또는 새로 생성
db = registerService("db", () => {
const pool = createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
port: 3306,
waitForConnections: true,
connectionLimit: 10, // 최대 연결 수 제한
queueLimit: 0 // 대기열 제한 없음
});
console.log("Database pool created");
return pool;
});
} catch (error) {
console.error("Failed to create database connection pool:", error);
throw new Error("Failed to initialize database connection");
}
/**
* executeQuery는 쿼리를 실행하고 결과를 반환하는 함수입니다.
* 쿼리 실행 중 에러가 발생하면 에러 메시지를 출력합니다.
*
* @param query - 실행할 SQL 쿼리 문자열
* @param values - 쿼리에 삽입할 값 배열
* @returns Promise<any> - 쿼리 결과를 반환하는 Promise
*/
async function executeQuery(query: string, values: any[] = []) {
if (!db) {
throw new Error("DB has not been initialized");
}
// 쿼리 실행 및 에러 처리
return new Promise((resolve, reject) => {
db.query(query, values, (error, results) => {
if (error) {
console.error("Database query error:", error);
return reject(error); // 에러 발생 시 Promise reject
}
resolve(results); // 성공 시 Promise resolve
});
});
}
export default executeQuery;
이제 Next.js
환경에서 MySQL과 연동하여 사용자 데이터를 가져오는 예시 컴포넌트를 만들겠습니다. 이 컴포넌트는 서버 측에서 데이터를 가져오며, 간단한 스타일을 추가해 테이블을 렌더링합니다.
import executeQuery from "@/lib/db"; // 위에서 만든 executeQuery 함수
// 서버 구성 요소 - 사용자 데이터를 DB에서 가져오는 함수
export default async function UsersPage() {
let users = [] as {
id: number;
name: string;
nickname: string;
email: string;
createdAt: string;
}[];
try {
// MySQL에서 사용자 데이터를 가져오는 쿼리 실행
users = (await executeQuery("SELECT * FROM users", [])) as {
id: number;
name: string;
nickname: string;
email: string;
createdAt: string;
}[];
} catch (error) {
console.error("Failed to load users:", error);
}
return (
<div>
<div style={styles.wrapper}>
<h2 style={styles.heading}>Users table</h2>
<table style={styles.table}>
<thead>
<tr style={styles.headerRow}>
<th style={styles.headerCell}>ID</th>
<th style={styles.headerCell}>Name</th>
<th style={styles.headerCell}>Nickname</th>
<th style={styles.headerCell}>Email</th>
<th style={styles.headerCell}>Created At</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id} style={styles.row}>
<td style={styles.cell}>{user.id}</td>
<td style={styles.cell}>{user.name}</td>
<td style={styles.cell}>{user.nickname}</td>
<td style={styles.cell}>{user.email}</td>
<td style={styles.cell}>
{new Date(user.createdAt).toLocaleDateString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
const styles = {
wrapper: {
width: "100%",
maxWidth: "1080px",
margin: "0 auto",
padding: "0 20px",
},
heading: {
textAlign: "center" as "center",
margin: "50px 0 20px 0",
fontSize: "24px",
fontWeight: "bold",
},
table: {
width: "100%",
borderCollapse: "collapse" as "collapse",
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
borderRadius: "10px",
overflow: "hidden",
},
headerRow: {
backgroundColor: "#0070f3",
color: "white",
},
headerCell: {
padding: "12px",
textAlign: "left" as "left",
fontSize: "14px",
fontWeight: "bold" as "bold",
borderBottom: "2px solid #ddd",
},
row: {
backgroundColor: "#f9f9f9",
transition: "background-color 0.3s",
},
cell: {
padding: "10px",
textAlign: "left" as "left",
borderBottom: "1px solid #ddd",
},
rowHover: {
backgroundColor: "#f1f1f1",
},
};
// app/server-actions/actions.ts
'use server';
import executeQuery from '@/lib/db';
/**
* Server Action 함수로 MySQL 데이터베이스에서 사용자를 추가하는 함수
* @param name - 사용자 이름
* @param nickname - 사용자 닉네임
* @param email - 사용자 이메일
*/
export async function addUser(name: string, nickname: string, email: string) {
try {
// INSERT 쿼리를 실행해 사용자 추가
await executeQuery(
'INSERT INTO users (name, nickname, email) VALUES (?, ?, ?)',
[name, nickname, email]
);
console.log("User added successfully");
} catch (error) {
console.error('Error adding user:', error);
throw new Error('Failed to add user');
}
}
/**
* Server Action 함수로 MySQL 데이터베이스에서 모든 사용자 데이터를 가져오는 함수
*/
export async function fetchUsers() {
try {
// SELECT 쿼리를 실행해 모든 사용자 데이터를 가져옴
const users = await executeQuery(
'SELECT id, name, nickname, email, createdAt FROM users',
[]
);
return users;
} catch (error) {
console.error('Error fetching users:', error);
throw new Error('Failed to fetch users');
}
}