[포스코x코딩온] 웹 풀스택 과정 7기 6주차 화요일 회고
TypeScript
과정에서 배우지는 않았지만 내용을 따라하면서 TypeScript를 사용해보았다.
그런데 무작정 사용하려고 하니 오류가 무더기로 발생했다.
이를 해결하기 위한 고군분투의 과정을 기록해둔다.
*.ts 파일 실행 오류
당연히 node가 *.ts
파일을 실행해 줄거라고 생각했는데 전혀 그렇지 않았다.
*.ts
파일을 실행하려면 몇 가지 과정이 필요했다.
먼저 typescript
와 ts-node
를 설치했다.
그리고 프로젝트 루트 폴더에서 tsc --init
명령어로 tsconfig.json
파일을 생성했다.
이후 ts-node
로 *.ts
파일을 실행시킬 수 있었다.
npm i typescript ts-node
npx tsc --init
npx ts-node src/index.ts
절대경로
매번 상대경로를 쓰다보니 import 경로가 너무 길어졌다.
그런데 React를 공부하다보니 절대경로를 사용하는 방법이 있어서 찾아보았다.
tsconfig.json
파일에 다음과 같은 설정을 추가하면 된다.
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["*"]
}
}
}
이 설정을 추가하면 @/
로 시작하는 경로를 절대경로로 사용할 수 있다.
다만 Node는 절대경로를 지원하지 않기 때문에 tsconfig-paths
라는 라이브러리를 사용해야 한다.
나는 다음 명령어를 index.sh
파일에 추가해두고 사용했다.
ts-node -r tsconfig-paths/register --files index.ts
Sequelize
Sequelize는 Node.js에서 SQL을 사용하기 위한 ORM(Object-Relational Mapping) 라이브러리이다.
npx sequelize init
명령어를 통해 Sequelize를 초기화할 수 있다.
해당 명령어를 실행하면 여러가지 파일들이 생성된다.
이후 models
폴더 안에 사용하고 싶은 테이블에 맞춰 다음과 같은 파일을 생성한다.
import { DataTypes, Sequelize } from "sequelize";
export default function TableName(sequelize: Sequelize, dataTypes: typeof DataTypes) {
return sequelize.define("TableName", {
/*
열이름: {
속성: 속성값
}
*/
id: {
type: dataTypes.INTEGER, // 타입
autoIncrement: true, // 자동 증가
primaryKey: true, // 기본키
allowNull: false // null 허용 여부
},
name: {
type: dataTypes.STRING(20),
}
// ...
}, {
tableName: "TableName", // 테이블 이름
freezeTableName: true, // 테이블 이름 고정 (false 일 경우 테이블 이름을 복수로 만들어버림)
timestamps: false, // createdAt, updatedAt 컬럼 생성 여부
});
}
이후 다음과 같이 사용할 수 있다.
import db from "@/models";
async function get(req: Request, res: Response) {
const row = await db.TableName.findByPk(/* pk */); // 기본키 값으로 조회
const rows = await db.TableName.findAll(); // 전체 조회
const rows = await db.TableName.findAll({where: {/* 조건 */},}); // 조건에 맞는 행 조회
const row = await db.TableName.create({/* 데이터 */}); // 데이터 생성
await db.TableName.update({/* 수정할 데이터 */}, {where: {/* 조건 */},}); // 조건에 맞는 데이터 수정
await db.TableName.destroy({where: {/* 조건 */},}); // 조건에 맞는 데이터 삭제
}
models/index.js
뜯어보기
Sequelize 초기화 시 생성되는 파일 중 models/index.js
라는 파일이 있다.
해당 파일을 import 하면 DB 객체를 얻을 수 있다.
그러나 내 프로젝트에서는 해당 파일에서 오류가 나왔다.
오류를 고치는 김에 TS로 작성하기 위해 직접 뜯어 봤다.
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const process = require('process');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
// config.json 파일을 불러와서 현재 환경에 맞는 설정을 가져옴
const db = {};
let sequelize; // config 파일에 맞춰 sequelize 객체 생성
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (
// models 폴더 안에 있는 파일들 중 index.js, *.test.js 파일은 제외한 모든 .js 파일을 불러옴
file.indexOf('.') !== 0 &&
file !== basename &&
file.slice(-3) === '.js' &&
file.indexOf('.test.js') === -1
);
})
.forEach(file => {
// 각 파일들을 불러와서 해당 파일의 default 함수에 sequelize, DataTypes 객체를 넘겨줌
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
// 그 값을 db 객체에 넣어줌
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
// db 객체에 있는 모델들을 순회하면서 associate 메소드가 있는 경우 이를 실행
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize; // db 객체에 sequelize 객체를 넣어줌
db.Sequelize = Sequelize;
module.exports = db; // db 객체를 export
먼저 나는 “type”: “module”를 지정했기 때문에 require
를 import
로 바꿔야 했다.
그렇게 되면 중간의 forEach 문에서 폴더 내 파일들을 불러올 수 없었다.
그래서 그냥 수동으로 불러왔다.
// models/index.ts
import { Sequelize, DataTypes } from "sequelize";
import process from "process";
import Visitor from "./visitor";
...
const visitor = Visitor(sequelize, DataTypes);
...
config.json
파일도 config/index.ts
파일로 바꿔서 불러왔다.
// config/index.ts
import { Dialect } from "sequelize";
interface Config {
username: string;
password: string;
database: string;
host: string;
dialect: Dialect;
}
interface Configs {
[key: string]: Config;
}
const configs: Configs = {
development: {
username: "u1",
password: "1",
database: "exerdb",
host: "localhost",
dialect: "mysql",
},
// test: {},
// production: {},
}
export default configs;
// models/index.ts
...
import configs from "@/config";
...
이를 수정했더니 fs
, path
모듈은 필요 없어져서 제거했다.
또 TS를 적용하니 sequelize
객체를 생성할 때 오류가 발생했다. 따라서 envVariable
변수를 따로 빼줬다.
...
const { database, username, password, use_env_variable, ...config } = configs[env];
const envVariable = use_env_variable && process.env[use_env_variable]
const sequelize = envVariable
? new Sequelize(envVariable, config)
: new Sequelize(database, username, password, config);
...
마지막으로 DB의 타입을 정의하고 export 해줬다.
...
interface DB {
sequelize: Sequelize;
Sequelize: typeof Sequelize;
visitor: typeof visitor;
}
export default <DB>{
sequelize,
Sequelize,
visitor,
};
최종 코드는 다음과 같다.
import { Sequelize, DataTypes } from "sequelize";
import process from "process";
import configs from "@/config";
import Visitor from "./visitor";
const env = process.env.NODE_ENV || "development";
const { database, username, password, use_env_variable, ...config } = configs[env];
const envVariable = use_env_variable && process.env[use_env_variable]
const sequelize = envVariable
? new Sequelize(envVariable, config)
: new Sequelize(database, username, password, config);
const visitor = Visitor(sequelize, DataTypes);
interface DB {
sequelize: Sequelize;
Sequelize: typeof Sequelize;
visitor: typeof visitor;
}
export default <DB>{
sequelize,
Sequelize,
visitor,
};
암호화
보안 문제는 어디서나 일어날 수 있는데 이는 서버 상에서도 마찬가지이다.
따라서 서버에서도 보안을 위해 암호화를 해야 한다.
Node 에서는 이를 위해 crypto
모듈을 제공한다.
crypto
import crypto from "crypto";
function createSaltHash(pw: string) {
const salt = crypto.randomBytes(8).toString("hex");
const hash = crypto.pbkdf2Sync(pw, salt, 100000, 64, "sha512").toString("hex");
return { salt, hash };
}
function comparePassword(pw: string, salt: string, hash: string) {
const hashedPW = crypto.pbkdf2Sync(pw, salt, 100000, 64, "sha512").toString("hex");
return hashedPW === hash;
}
createSaltHash
함수를 통해 salt 값과 pw의 해시값을 생성한 뒤 유저의 정보로 저장한다.
이후 비밀번호를 확인할 때 comparePassword
함수를 통해 비밀번호가 일치하는지 확인한다.
bcrypt
bcrypt
는 외부 암호화 라이브러리이다.
crypto
보다 쉽게 사용할 수 있다는 장점이 있다.
import bcrypt from "bcrypt";
async function createSaltHash(pw: string) {
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(pw, salt);
return { salt, hash };
}
async function comparePassword(pw: string, salt: string, hash: string) {
const hashedPW = await bcrypt.hash(pw, salt);
return hashedPW === hash;
}
메소드 이름 뒤에 Sync
를 붙이면 동기함수로 사용할 수 있다.
하지만 공식문서는 비동기 함수를 권장한다.
이벤트 루프를 막아 앱의 성능을 저하시킬 수 있기 때문이다.