728x90
반응형
Node.js 애플리케이션에서 MySQL 데이터베이스를 연동하는 방법을 알아봅니다. mysql2 패키지를 중심으로 연결 설정부터 CRUD 작업, 트랜잭션 처리까지 실무에서 바로 사용할 수 있는 내용을 다룹니다.
1. mysql2 패키지 소개
mysql vs mysql2 패키지 비교
Node.js에서 MySQL을 연동할 때 주로 사용하는 패키지는 mysql과 mysql2 두 가지입니다.
mysql2를 선택해야 하는 이유:
- Promise 네이티브 지원: mysql2는
.promise()메서드를 통해 Promise 기반 API를 기본 제공합니다 - Prepared Statement 지원: 서버 사이드 Prepared Statement를 지원하여 보안과 성능이 향상됩니다
- 더 빠른 성능: mysql 패키지 대비 파싱 속도가 개선되었습니다
- mysql 패키지 호환성: 기존 mysql 패키지와 API가 호환되어 마이그레이션이 쉽습니다
// mysql 패키지 (콜백 기반)
const mysql = require('mysql');
// mysql2 패키지 (Promise 지원)
const mysql = require('mysql2/promise');
2. 설치 및 기본 연결 설정
패키지 설치
npm install mysql2
기본 연결 설정
const mysql = require('mysql2/promise');
async function connectDatabase() {
const connection = await mysql.createConnection({
host: 'localhost',
port: 3306,
user: 'root',
password: 'your_password',
database: 'your_database'
});
console.log('MySQL 연결 성공');
return connection;
}
연결 옵션 상세
const connectionConfig = {
host: 'localhost', // MySQL 서버 호스트
port: 3306, // MySQL 포트 (기본값: 3306)
user: 'root', // 사용자명
password: 'password', // 비밀번호
database: 'mydb', // 데이터베이스명
charset: 'utf8mb4', // 문자셋 (이모지 지원 시 utf8mb4)
timezone: '+09:00', // 타임존 설정
connectTimeout: 10000, // 연결 타임아웃 (ms)
waitForConnections: true, // 연결 대기 여부
dateStrings: true // 날짜를 문자열로 반환
};
3. 연결 풀(Connection Pool) 사용법
단일 연결 대신 연결 풀을 사용하면 여러 요청을 효율적으로 처리할 수 있습니다. 실무에서는 연결 풀 사용을 권장합니다.
연결 풀 생성
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'your_database',
waitForConnections: true,
connectionLimit: 10, // 최대 연결 수
queueLimit: 0, // 대기열 제한 (0 = 무제한)
enableKeepAlive: true, // Keep-Alive 활성화
keepAliveInitialDelay: 0 // Keep-Alive 초기 지연
});
연결 풀 사용 예제
// 방법 1: pool.query() 직접 사용 (권장)
async function getUsers() {
const [rows] = await pool.query('SELECT * FROM users');
return rows;
}
// 방법 2: connection 획득 후 사용
async function getUserById(id) {
const connection = await pool.getConnection();
try {
const [rows] = await connection.query(
'SELECT * FROM users WHERE id = ?',
[id]
);
return rows[0];
} finally {
connection.release(); // 반드시 연결 반환
}
}
연결 풀의 장점:
- 연결 재사용으로 오버헤드 감소
- 동시 요청 처리 능력 향상
- 자동 연결 관리 (끊어진 연결 재생성)
반응형
4. Prepared Statement와 SQL Injection 방지
SQL Injection 위험 예시
// 위험한 코드 - SQL Injection 취약
const userId = "1 OR 1=1"; // 악의적인 입력
const query = `SELECT * FROM users WHERE id = ${userId}`;
// 결과: SELECT * FROM users WHERE id = 1 OR 1=1 (모든 데이터 노출)
Prepared Statement 사용 (안전한 방법)
// 안전한 코드 - Placeholder 사용
async function getUserSafe(userId) {
const [rows] = await pool.query(
'SELECT * FROM users WHERE id = ?',
[userId]
);
return rows;
}
// 여러 파라미터 사용
async function searchUsers(name, email) {
const [rows] = await pool.query(
'SELECT * FROM users WHERE name LIKE ? AND email = ?',
[`%${name}%`, email]
);
return rows;
}
Named Placeholder 사용
async function getUserByNamedParams(params) {
const [rows] = await pool.query(
'SELECT * FROM users WHERE name = :name AND age > :age',
{ name: params.name, age: params.age }
);
return rows;
}
execute() 메서드로 Prepared Statement 사용
// execute()는 서버 사이드 Prepared Statement 사용
async function getUserWithPrepared(userId) {
const [rows] = await pool.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
return rows;
}
query()와 execute()의 차이점:
query(): 클라이언트 사이드에서 파라미터 이스케이프 처리execute(): 서버 사이드 Prepared Statement 사용 (동일 쿼리 반복 시 성능 이점)
5. CRUD 작업 예제
Create (생성)
async function createUser(user) {
const [result] = await pool.query(
'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
[user.name, user.email, user.age]
);
console.log('삽입된 ID:', result.insertId);
console.log('영향받은 행:', result.affectedRows);
return result.insertId;
}
// 여러 행 삽입
async function createUsers(users) {
const values = users.map(u => [u.name, u.email, u.age]);
const [result] = await pool.query(
'INSERT INTO users (name, email, age) VALUES ?',
[values]
);
return result.affectedRows;
}
Read (조회)
// 전체 조회
async function getAllUsers() {
const [rows] = await pool.query('SELECT * FROM users');
return rows;
}
// 조건 조회
async function getUsersByAge(minAge) {
const [rows] = await pool.query(
'SELECT * FROM users WHERE age >= ? ORDER BY age DESC',
[minAge]
);
return rows;
}
// 페이징 처리
async function getUsersWithPaging(page, limit) {
const offset = (page - 1) * limit;
const [rows] = await pool.query(
'SELECT * FROM users LIMIT ? OFFSET ?',
[limit, offset]
);
const [[{ total }]] = await pool.query(
'SELECT COUNT(*) as total FROM users'
);
return {
data: rows,
total,
page,
totalPages: Math.ceil(total / limit)
};
}
Update (수정)
async function updateUser(id, updates) {
const [result] = await pool.query(
'UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?',
[updates.name, updates.email, updates.age, id]
);
if (result.affectedRows === 0) {
throw new Error('사용자를 찾을 수 없습니다');
}
return result.affectedRows;
}
// 동적 업데이트 (변경된 필드만)
async function updateUserPartial(id, updates) {
const fields = [];
const values = [];
for (const [key, value] of Object.entries(updates)) {
fields.push(`${key} = ?`);
values.push(value);
}
if (fields.length === 0) {
throw new Error('업데이트할 필드가 없습니다');
}
values.push(id);
const [result] = await pool.query(
`UPDATE users SET ${fields.join(', ')} WHERE id = ?`,
values
);
return result.affectedRows;
}
Delete (삭제)
async function deleteUser(id) {
const [result] = await pool.query(
'DELETE FROM users WHERE id = ?',
[id]
);
return result.affectedRows > 0;
}
// 조건부 삭제
async function deleteInactiveUsers(days) {
const [result] = await pool.query(
'DELETE FROM users WHERE last_login < DATE_SUB(NOW(), INTERVAL ? DAY)',
[days]
);
return result.affectedRows;
}
6. 트랜잭션 처리
여러 쿼리를 하나의 작업 단위로 처리해야 할 때 트랜잭션을 사용합니다. 모든 쿼리가 성공하면 커밋하고, 하나라도 실패하면 롤백합니다.
기본 트랜잭션 사용
async function transferMoney(fromId, toId, amount) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// 출금
await connection.query(
'UPDATE accounts SET balance = balance - ? WHERE id = ?',
[amount, fromId]
);
// 입금
await connection.query(
'UPDATE accounts SET balance = balance + ? WHERE id = ?',
[amount, toId]
);
// 거래 기록
await connection.query(
'INSERT INTO transactions (from_id, to_id, amount) VALUES (?, ?, ?)',
[fromId, toId, amount]
);
await connection.commit();
console.log('거래 완료');
} catch (error) {
await connection.rollback();
console.error('거래 실패, 롤백됨:', error.message);
throw error;
} finally {
connection.release();
}
}
트랜잭션 유틸리티 함수
async function withTransaction(callback) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const result = await callback(connection);
await connection.commit();
return result;
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}
// 사용 예시
async function createOrderWithItems(order, items) {
return withTransaction(async (conn) => {
const [orderResult] = await conn.query(
'INSERT INTO orders (user_id, total) VALUES (?, ?)',
[order.userId, order.total]
);
const orderId = orderResult.insertId;
for (const item of items) {
await conn.query(
'INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)',
[orderId, item.productId, item.quantity]
);
}
return orderId;
});
}
7. Promise/async-await 패턴 활용
콜백에서 Promise로 전환
// 콜백 방식 (레거시)
const mysql = require('mysql2');
const pool = mysql.createPool(config);
pool.query('SELECT * FROM users', (error, results) => {
if (error) {
console.error(error);
return;
}
console.log(results);
});
// Promise 방식 (권장)
const mysql = require('mysql2/promise');
const pool = mysql.createPool(config);
const [rows] = await pool.query('SELECT * FROM users');
console.log(rows);
병렬 쿼리 실행
async function getDashboardData(userId) {
const [userInfo, orders, notifications] = await Promise.all([
pool.query('SELECT * FROM users WHERE id = ?', [userId]),
pool.query('SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 10', [userId]),
pool.query('SELECT * FROM notifications WHERE user_id = ? AND is_read = 0', [userId])
]);
return {
user: userInfo[0][0],
recentOrders: orders[0],
unreadNotifications: notifications[0]
};
}
에러 핸들링
async function safeQuery(query, params = []) {
try {
const [rows] = await pool.query(query, params);
return { success: true, data: rows };
} catch (error) {
console.error('쿼리 실행 오류:', error.message);
if (error.code === 'ER_DUP_ENTRY') {
return { success: false, error: '중복된 데이터입니다' };
}
if (error.code === 'ER_NO_REFERENCED_ROW_2') {
return { success: false, error: '참조하는 데이터가 존재하지 않습니다' };
}
return { success: false, error: '데이터베이스 오류가 발생했습니다' };
}
}
Express와 함께 사용하기
const express = require('express');
const mysql = require('mysql2/promise');
const app = express();
app.use(express.json());
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'mydb',
connectionLimit: 10
});
// 사용자 목록 조회
app.get('/users', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM users');
res.json(rows);
} catch (error) {
res.status(500).json({ error: '서버 오류' });
}
});
// 사용자 생성
app.post('/users', async (req, res) => {
try {
const { name, email } = req.body;
const [result] = await pool.query(
'INSERT INTO users (name, email) VALUES (?, ?)',
[name, email]
);
res.status(201).json({ id: result.insertId });
} catch (error) {
res.status(500).json({ error: '서버 오류' });
}
});
app.listen(3000, () => {
console.log('서버 시작: http://localhost:3000');
});
결론
Node.js에서 MySQL 연동 시 mysql2 패키지를 사용하면 Promise 지원과 Prepared Statement를 통해 안전하고 효율적인 데이터베이스 작업이 가능합니다. 실무에서는 연결 풀을 활용하고, Placeholder를 통한 SQL Injection 방지, 그리고 트랜잭션을 적절히 사용하여 데이터 무결성을 보장하는 것이 중요합니다.
728x90
반응형
'Node.js' 카테고리의 다른 글
| Node.js의 Mongoose 사용법 완벽 가이드 (0) | 2026.03.17 |
|---|---|
| Node.js MongoDB 연동 (0) | 2026.03.17 |
| Node.js 데이터베이스 연동(Database Integration) 완벽 가이드 (1) | 2026.03.16 |
| Node.js의 쿠키 관리(Cookie Management) (0) | 2026.03.16 |
| Node.js의 세션 관리(Session Management) (0) | 2026.03.15 |