컴동쌤은 iOS 개발자 지망생입니다. 여태까지한 공부는 대부분 iOS였습니다. 하지만 예전에 잠깐 서버를 배운 적이 있습니다. 그래서 nodejs로 간단한 서버를 만들어서 지금까지 개발한 앱에 적용해봅시다. 물론 아주 간단한 서버이고 서버에 대해서는 잘 모르기 때문에 코드도 iOS 만큼 자세하게 소개해드리지는 못할 것 같고요. 기능 위주로 간단하게만 소개해드릴 생각입니다.
컴동쌤은 타입스크립트를 배웠습니다만... 애초에 공부한 이유가 OOP에 대한 개념을 익혀서 Swift를 더 잘하기 위함이었고 (Swift로 OOP의 기초부터 가르치는 수업을 찾는 것이 너무 힘들더군요ㅠㅠ) 서버를 만들 때 적용해본 적은 없습니다.
그래서 이번에는 과감하게 TypeScript를 배제하고 서버를 만들어보겠습니다. 고민을 많이 해보았는데 문법과 개념을 알아도 서버에 적용하기는 쉽지 않을 것 같더라고요.
그래서 이번에는 순수 js로만 만들고 나중에 별도의 프로젝트를 통해 차근차근 고쳐서 써보겠습니다.
컴동쌤은 서버를 배우면서 AWS도 배웠었습니다. 도메인을 구매해서 연결하고 ec2 인스턴스를 만들고 RDS를 통해 DB를 만들어서 서버를 만들었습니다. 하지만 이번 프로젝트에서는 과감하게 생략하겠습니다. 서버는 로컬 호스트를 통해서 돌리고 DB도 제 노트북에 설치할 예정입니다.
CORS이나 Header에 관련된 보안과 서버에 로그를 남기는 것도 과감히 생략하겠습니다. 기능에 집중할 예정입니다.
스크립트를 통해서 더미 데이터를 만들어서 DB에 넣어줍시다. 랜덤 이름 생성하는 함수를 만들었고 각 반 별로 학생 수가 다르도록 설계했습니다. 아래 스크립트를 grade를 바꾸어서 3번 실행하여 더미 데이터를 만듭시다.
const { pool } = require('../config/database.js')
// id 만들어주는 함수
function idMaker(student) {
const year = "2021"
const grade = student.grade
const classNumber = String(student.classNumber)
const number = String(student.number)
return `${year}${grade}${classNumber.padStart(2, '0')}${number.padStart(2, '0')}`
}
// 학생 데이터 insert하는 함수
async function insertStudent(connection, student) {
const id = idMaker(student)
const url = "https://picsum.photos/200"
params = [id, student.grade, student.classNumber, student.number, student.name, url]
const insertStudentQuery = `
INSERT INTO Student(id, grade, classNumber, number, name, profileImageURL)
VALUES (?, ?, ?, ?, ?, ?);
`;
const insertDealRow = await connection.query(
insertStudentQuery,
params
);
return insertDealRow;
};
// 랜덤 이름을 만드는 함수
function createRandomName() {
const lastNames = ["김", "이", "박", "최", "윤", "문", "성", "설", "정", "구", "양", "민", "고", "홍", "장", "강"]
const firstNames = ["철수", "영수", "민수", "민준", "준혁", "상혁", "정혁", "준서", "문규", "강호", "상민", "철민", "성혁", "민혁", "민규", "영희", "민희", "서아", "시아", "설아", "수아", "지민", "민서", "민지", "유빈", "혜림", "규리", "윤희", "윤지", "예지"]
const randomLastNameIdx = Math.floor(Math.random() * lastNames.length);
const randomfirstNameIdx = Math.floor(Math.random() * firstNames.length);
return `${lastNames[randomLastNameIdx]}${firstNames[randomfirstNameIdx]}`
}
// 더미 데이터 넣는 함수
createDummyStudents = async function (student) {
const connection = await pool.promise().getConnection(async (conn) => conn);
const grade = 3
for (let i = 1; i < 15; i++) {
let classNumber = i
for (let j = 1; j < 30 + i; j++) {
let number = j
let name = createRandomName()
let student = { grade: grade, classNumber: classNumber, number: number, name: name}
const result = await insertStudent(connection, student)
console.log(result)
}
}
connection.release();
return
};
createDummyStudents()
console.log("finished")
서버는 router-controller-provider-dao 구조를 가지도록 만들겠습니다.
실습의 생산성을 위해 최소한의 미들웨어만 사용합니다. 말씀 드렸듯이 helmet 같은 보안 관련 미들웨어, morgan 같은 로그 관련 미들웨어를 생략했습니다.
const express = require('express');
const studentRouter = require('./routes/student.js');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/students', studentRouter);
// 404 에러 처리
app.use((req, res, next) => {
res.status(404).send('Not Found');
});
// 서버 에러 처리
app.use((err, req, res, next) => {
console.error(err);
res.sendStatus(500);
});
app.listen(8080);
클라이언트 개발자를 위해서 response에 담을 메타 데이터를 미리 정의합니다.
// 성공 및 에러 정의
const response = ({isSuccess, code, message}, result) => {
return {
isSuccess: isSuccess,
code: code,
message: message,
result: result
}
};
const errResponse = ({isSuccess, code, message}) => {
return {
isSuccess: isSuccess,
code: code,
message: message
}
};
module.exports = { response, errResponse };
// 메시지 모아 놓기
module.exports = {
// Success
SUCCESS : { "isSuccess": true, "code": 1000, "message":"성공" },
// Error
DB_ERROR : { "isSuccess": false, "code": 4000, "message": "데이터 베이스 에러"},
SERVER_ERROR : { "isSuccess": false, "code": 4001, "message": "서버 에러"},
}
/**
* API No. 1
* API Name : 학생 조회 API
* [GET] /students/totalNumber
*/
exports.getTotalNumber = async function (req, res) {
const result = await studentsProvider.getTotalNumber()
return res.send(response(responses.SUCCESS, result));
};
먼저 학년 별로 반 갯수를 찾아서 넣어 오브젝트를 만듭니다. 그 데이터를 이용해서 다시 반 갯수만큼 반복해서 반별 학생 수를 찾습니다.
// 학년별 반, 반별 학생수 찾기
exports.getTotalNumber = async function (studentID) {
const connection = await pool.promise().getConnection(async (conn) => conn);
// 학년별 반 갯수 찾기
let numOfClassResult = {}
for (let grade = 1; grade < 4; grade++) {
const numOfClass = await studentDao.countNumOfClass(connection, grade)
numOfClassResult[grade] = numOfClass[0].cnt
}
// 반별 학생 수 찾기
let numOfStudentsResult = {1: {}, 2: {}, 3: {}}
for (let grade = 1; grade < 4; grade++) {
const numOfClassOfGrade = numOfClassResult[grade]
for (let classNum = 1; classNum <= numOfClassOfGrade; classNum++) {
params = [grade, classNum]
const numOfStudents = await studentDao.countNumOfStudent(connection, params)
numOfStudentsResult[grade][classNum] = numOfStudents[0].cnt
}
}
// 결과 취합
result = {
numOfClasses: numOfClassResult,
numOfStudents: numOfStudentsResult
}
connection.release();
return result;
};
count를 이용해서 갯수를 셉니다. 특히 반은 여러 같은 값이 있으므로 DISTINCT를 통해 중복된 것을 제거해야 합니다.
exports.countNumOfClass = async function (connection, grade) {
const query = `
SELECT COUNT(DISTINCT classNumber) AS cnt
FROM Student
WHERE grade = ?;
`;
const [rows] = await connection.query(query, grade);
return rows;
}
exports.countNumOfStudent = async function (connection, params) {
const query = `
SELECT COUNT(*) AS cnt
FROM Student
WHERE grade = ? AND classNumber = ?;
`;
const [rows] = await connection.query(query, params);
return rows;
}
쿼리 스트링을 통해 받아온 id를 이용합니다. 클라이언트 측에서 공유된 규칙에 따라 id를 포함해서 요청을 보낼 것입니다.
/**
* API No. 2
* API Name : 학생 조회 API
* [GET] /students/search
*/
exports.getStudentInfo = async function (req, res) {
/**
* Query String: studentID
*/
const studentID = req.query.id;
// TODO: 학번 형식적 검증
const result = await studentsProvider.getStudentInfo(studentID)
return res.send(response(responses.SUCCESS, result));
};
// 학번으로 학생 찾기
exports.getStudentInfo = async function (studentID) {
const connection = await pool.promise().getConnection(async (conn) => conn);
const selectStudentResult = await studentDao.selectStudent(connection, studentID);
connection.release();
return selectStudentResult[0];
};
exports.selectStudent = async function (connection, studentID) {
const query = `
SELECT *
FROM Student
WHERE id = ?;
`;
const [rows] = await connection.query(query, studentID);
return rows;
}
localhost:8080/students/totalNumber
{
"isSuccess": true,
"code": 1000,
"message": "성공",
"result": {
"numOfClasses": {
"1": 10,
"2": 11,
"3": 14
},
"numOfStudents": {
"1": {
"1": 30,
"2": 31,
"3": 32,
"4": 33,
"5": 34,
"6": 35,
"7": 36,
"8": 37,
"9": 38,
"10": 39
},
"2": {
"1": 30,
"2": 31,
"3": 32,
"4": 33,
"5": 34,
"6": 35,
"7": 36,
"8": 37,
"9": 38,
"10": 39,
"11": 40
},
"3": {
"1": 30,
"2": 31,
"3": 32,
"4": 33,
"5": 34,
"6": 35,
"7": 36,
"8": 37,
"9": 38,
"10": 39,
"11": 40,
"12": 41,
"13": 42,
"14": 43
}
}
}
}
localhost:8080/students/search?id=202131101
{
"isSuccess": true,
"code": 1000,
"message": "성공",
"result": {
"id": 202131102,
"grade": 3,
"classNumber": 11,
"number": 2,
"name": "구예지",
"profileImageURL": "https://picsum.photos/200",
"status": "재학",
"createdAt": "2021-12-21T02:09:11.000Z",
"updatedAt": "2021-12-21T02:09:11.000Z"
}
}