DB에 연결하기 위해 프로젝트 루트 (backend, frontend 폴더내부가 아닌)에 .env 파일을 생성하고 작성합니다.
.env
DB_HOST= // DB의 호스트명
DB_USER= // DB 유저
DB_PASSWORD=// DB 비밀번호
DB_DATABASE= // 연결할 데이터베이스
DB_PORT= // DB 포트번호
파일을 준비했으면
yarn add dotenv @types/dotenv reflect-metadata typeorm
명령어로 dotenv와 reflect-metadata, 그리고 typeorm을 다운받아줍니다.
이후 src/config
디렉토리에 db.ts
파일을 생성합니다.
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import dotenv from 'dotenv';
dotenv.config({ path: '../.env' });
const { DB_DATABASE, DB_USER, DB_HOST, DB_PASSWORD } = process.env; //.env파일에 작성한 정보들을 저장
export const AppDataSource = new DataSource({
type: 'mysql',
host: DB_HOST,
port: 3306,
username: DB_USER,
password: DB_PASSWORD,
database: DB_DATABASE,
synchronize: false, // true로 해놓을 경우 DB에 이미 테이블이 있을경우 'Table already exist' 에러가 발생한다.
logging: false,
entities: [], // 사용할 엔티티. 현재는 없기때문에 비워놓았다.
migrations: [],
subscribers: []
}); // DB의 연결정보와 사용할 정보들을 작성
// createConnection() 이었지만 현재는 deprecated되었고 initialize()를 사용한다.
AppDataSource.initialize()
.then(() => {
console.log('Database connection established successfully');
// DB연결 성공시 문구 출력
})
.catch(e => {
console.error(e);
});
이후 yarn dev
명령어로 서버를 실행하겠습니다.
DB가 연결되어서 문구가 정상적으로 출력됩니다.
이제 db연결 정보는 작성했으니 나머지 필요한 폴더와 파일들을 src
폴더안에 생성하겠습니다.
controller/userController.ts
entity/User.ts
model/userModel.ts
routes/userRouter.ts
service/userService.ts
project root/backend
├── package.json
├── src
│ ├── app.ts
│ ├── config
│ │ └── db.ts
│ ├── controller
│ │ └── userController.ts
│ ├── entity
│ │ └── User.ts
│ ├── model
│ │ └── userModel.ts
│ ├── routes
│ │ └── userRouter.ts
│ ├── service
│ │ └── userService.ts
├── tsconfig.json
├── yarn-error.log
└── yarn.lock
import userRouter from './routes/userRouter
와app.use('/api/user', userRouter)
두 줄을 추가합니다.import express, { Request, Response } from 'express';
import cors from 'cors';
import userRouter from './routes/userRouter';
const app = express();
const PORT = 3099;
app.use(cors({ credentials: true, origin: 'http://localhost:3000' }));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/api/user', userRouter);
app.listen(PORT, () => {
console.log(`-------------SERVER LISTENING ON PORT ${PORT}-------------`);
});
앞으로 localhost:3099/api/user
로 들어오는 모든 요청은 userRouter로 라우팅 됩니다.
import express from 'express';
import { register } from '../controller/userController';
const userRouter = express.Router();
userRouter.post('/register', register); // /register요청은 post이고 이 경로로 요청할 경우 Controller의 register를 실행
export default userRouter;
위의 app.ts
에서 /api/user
로 들어오는 요청은 userRouter로 라우팅 되고, userRouter에 /register
로 요청할 경우 controller의 register 함수를 실행합니다. 즉 localhost:3099/api/user/register
로 요청하면 register를 실행합니다.
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('USER')
export class User {
@PrimaryGeneratedColumn() // 기본키
user_id!: number;
@Column({ type: 'varchar', nullable: false })
name!: string;
@Column({ type: 'varchar', nullable: false })
email!: string;
@Column({ type: 'varchar', nullable: false })
uid!: string;
@Column({ type: 'varchar', nullable: false })
pwd!: string;
@Column({ type: 'varchar', nullable: false })
nickname!: string;
@Column({ type: 'varchar', nullable: false })
phone!: string;
@Column({ type: 'varchar', nullable: false })
address!: string;
@Column({ type: 'date', nullable: false })
birth!: Date;
@Column({ type: 'varchar', nullable: true })
banknum?: string;
@Column({ type: 'varchar', nullable: true })
bank?: string;
@Column({ type: 'int', nullable: true })
grade?: number;
}
typeorm의 entity 작성법 입니다. @Entity
데코레이터를 사용해 Entity임을 명시하고 기본키에는 @PrimaryGeneratedColumn()
을 사용해 명시합니다.
일반 컬럼에는 @Column
을 사용합니다.
export const AppDataSource = new DataSource({
type: 'mysql',
host: DB_HOST,
port: 3306,
username: DB_USER,
password: DB_PASSWORD,
database: DB_DATABASE,
synchronize: false,
logging: false,
entities: [User], //User entity를 사용
migrations: [],
subscribers: []
});
User entity 를 사용할것이니 User를 entity에서 import후 entities에 추가합니다.
export interface UserInterface {
name: string;
email: string;
uid: string;
pwd: string;
nickname: string;
phone: string;
address: string;
birth: Date;
banknum: string;
bank: string;
}
이렇게 사용자 정보를 하나의 인터페이스로 정의함으로써 코드에서 사용자 객체를 다룰 때 일관된 구조를 가질 수 있습니다. 또한 TypeScript의 정적 타입 검사 기능을 활용하여 코드의 안정성을 높일 수 있습니다. 수정이 생긴다면 이 부분만 고쳐주면 되어서 인터페이스를 사용하면 유지보수성을 향상시킬 수 있습니다.
import { Request, Response } from 'express';
import { UserInterface } from '../model/userModel';
import { registerService } from '../service/userService';
export async function register(req: Request<{}, {}, UserInterface>, res: Response) {
try {
const userData = await registerService(req.body);
console.log(userData);
res.json({ msg: `${userData.uid}님 회원가입이 완료되었습니다.` });
} catch (e) {
console.log(e);
res.status(500).json({ msg: '회원가입 실패' });
}
}
이 부분은 Express에서 제공되는 Request 객체의 타입을 지정하는 부분입니다.
Request<{}, {}, UserInterface>
: 이것은 TypeScript의 제네릭을 사용하여 Request
객체의 타입을 지정하는 부분입니다.
첫 번째 제네릭 ({}
): HTTP 요청의 파라미터(쿼리스트링, 루트 매개변수 등)에 대한 타입을 나타냅니다. 여기서는 비어있는 객체 {}
로 표시되어 있으므로, 요청 파라미터의 타입이 없음을 의미합니다.
두 번째 제네릭 ({}
): HTTP 요청의 바디에 대한 타입을 나타냅니다. 여기서도 비어있는 객체 {}
로 표시되어 있으므로, 요청 바디의 타입이 없음을 의미합니다.
세 번째 제네릭 (UserInterface
): HTTP 요청에 포함되는 사용자 정보에 대한 타입을 나타냅니다. 이 부분이 중요한데 UserInterface
는 위에서 작성한 model/userModel.ts
에서 정의한 사용자 정보를 담고 있는 TypeScript 인터페이스입니다. 이를 통해 요청 바디가 예상대로 사용자 정보를 포함하고 있음을 명시합니다.
즉, Request
객체가 빈 쿼리스트링과 빈 바디를 가지며, 사용자 정보는 UserInterface
에 정의된 형식을 따라야 한다고 명시한 것입니다.
이후 try-catch
문으로 감싸주고 registerService
에 req.body
를 매개변수로 담아주고 그 결과를 userData에 저장해서 출력해 봅니다. 이후 Response
객체에 json 형태로 회원가입이 완료되었다는 응답을 합니다.
import { AppDataSource } from '../config/db';
import { User } from '../entity/User';
import { UserInterface } from '../model/userModel';
import bcrypt from 'bcrypt';
const repository = AppDataSource.getRepository(User);
export async function registerService(userRegisterData: UserInterface) {
try {
// eslint-disable-next-line prefer-const
let { name, email, uid, pwd, nickname, phone, address, birth, banknum, bank } = userRegisterData;
const grade = 0; // 회원 등급 (일반회원, 관리자)
// 비밀번호 암호화
const salt = await bcrypt.genSalt(10);
pwd = await bcrypt.hash(pwd, salt);
const user = repository.create({ name, email, uid, pwd, nickname, phone, address, birth, banknum, bank, grade });
const userData = await repository.save(user);
return userData;
} catch (e) {
console.log(e);
throw new Error('회원가입 실패');
}
}
실제 회원가입 로직을 구현하는 service부분입니다.
const repository = AppDataSource.getRepository(User);
비밀번호 암호화를 할 것이기 때문에 yarn add bcrypt @types/bcrypt
명령어로 bcrypt를 설치합니다. 암호화에 대한 내용은 다음 포스팅에서 다루겠습니다.
controller에서 넘어온 유저 정보를 객체 분해 할당(Object Destructuring Assignment) 으로 각 변수에 할당합니다.
grade는 유저가 정하는 것이 아니기 때문에 0으로 초기화 했습니다.
이후 그 변수들을
repository.create()
함수 안에 담아서 데이터를 생성합니다. 이정보는 User entity에 맞춰서 생성됩니다.
repository.save()
함수로 저장되고, 이에 대한 정보를 userData변수에 저장하고 controller에 반환합니다.
이제 백엔드는 완료되었으니 화면을 만들어보겠습니다.
회원가입 화면을 생성하겠습니다 위치는 frontend/pages/user/register.tsx
project root/frontend
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── globals.css
│ ├── index.tsx
│ └── user
│ └── register.tsx
├── postcss.config.js
├── public
│ ├── next.svg
│ └── vercel.svg
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
Next.js의 라우팅 덕분에 localhost:3000/user/register
로 접속하면 register.tsx
를 렌더링 합니다.
import React from 'react';
fimport React from 'react';
function Register() {
return (
<div className="flex flex-col items-center justify-center min-h-screen ">
<form className="flex flex-col items-center justify-center min-h-screen py-2">
<label htmlFor="uid">ID</label>
<input
className="p-2 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:border-gray-600 bg-persevi-grey text-black"
id="uid"
type="text"
placeholder="ID"
required
/>
<label htmlFor="pwd">Password</label>
<input
className='p-2 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:border-gray-600 bg-persevi-grey text-black'
id="pwd"
type="password"
placeholder="비밀번호"
required
/>
<label htmlFor="name text-black">Name</label>
<input
className="p-2 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:border-gray-600 bg-persevi-grey text-black"
id="name"
type="text"
placeholder="이름"
required
/>
<label htmlFor="email">Email</label>
<input
className='p-2 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:border-gray-600 bg-persevi-grey text-black'
id="email"
type="text"
placeholder="이메일"
required
/>
<label htmlFor="nickname">Nickname</label>
<input
className="p-2 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:border-gray-600 bg-persevi-grey text-black"
id="nickname"
type="text"
placeholder="닉네임"
required
/>
<label htmlFor="phone">Phone</label>
<input
className='p-2 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:border-gray-600 bg-persevi-grey text-black'
id="phone"
type="text"
placeholder="전화번호"
required
/>
<label htmlFor="birth">생년월일</label>
<input
className="p-2 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:border-gray-600 bg-persevi-grey text-black"
id="birth"
type="date"
required
/>
<button className="p-2 border rounded-lg mb-4 bg-persevi-blue" type="submit">
회원가입
</button>
</form>
</div>
);
}
export default Register;
클래스 네임의 persevi-(color)
는 제가 tailwindCSS의 클래스명으로 만들어둔 색 입니다.
yarn dev
localhost:3000/user/register
로 접속 해보겠습니다.
화면이 정상적으로 나오는걸 확인했으니 이제 기능을 추가하겠습니다.
const [msg, setMsg] = useState('');
const [formData, setFormData] = useState({
name: '',
email: '',
uid: '',
pwd: '',
nickname: '',
phone: '',
birth: ''
});
먼저 회원가입을 하면 띄워줄 메세지를 저장할 State와 input의 정보 총 6개가 저장되는 formData를 만들었습니다. 6개 모두 따로 State를 만들어도 되지만, 이렇게 객체로 생성하는것이 효과적입니다.
현재코드는 버튼을 아무리 눌러도 아무런 변화가 일어나지 않습니다. 그렇기 때문에 axios로 input의 값들을 서버에게 넘기며 register요청을 하는 함수를 작성하겠습니다.
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); //기본 이벤트인 새로고침을 방지
axios
.post('http://localhost:3099/api/user/register', formData)
//생성한 formData State를 전달하며 요청
.then(res => {
console.log(res);
})
.catch(e => {
console.log(e);
});
}
이후 <form>
태그의 onSubmit
속성에 추가해줍니다.
<form onSubmit={onSubmit}>
formData의 값을 채워줄 onChange
함수를 작성합니다.
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
setFormData({...formData, [e.target.id]:e.target.value } )
}
이 함수는 인풋의 값이 바뀔때마다 그 값을 formData에 채워주는 역할을 합니다.
파라미터로 받는 e는 현재 이벤트가 발생하는 개체입니다.
{...formData}
: Javascript의 Spread 연산입니다. formData를 복사합니다.[e.target.id]
: 객체의 키값을 동적으로 할당하기위해 괄호로 감싸고, 객체의 키값은 현재 이벤트가 일어나고 있는 개체의 id값입니다. <input>
태그에 id값을 formData
의 키값과 똑같이 설정했으므로, email인풋을 입력하고 있다면 formData
의 email키를 말합니다.:e.target.value
: 현재 이벤트가 일어나고 있는 개체(input)의 값입니다.email 인풋을 작성중이라 가정할때 현재 formData
를 복사한 후 인풋의 id값인 email에 현재 value를 넣습니다.
즉, formData[email] = 현재 input value
와 같습니다.
이 함수를 모든 <input>
태그의 onChange
속성에 추가합니다.
여기까지 했다면 회원가입 기능은 작동하고 데이터베이스에 추가될 것입니다. 확인해보겠습니다.
test정보를 작성하고 db를 확인해보면
mysql> select * from user;
+---------+------+---------------+------+--------------------------------------------------------------+----------+---------------+---------+------------+---------+------+-------+
| user_id | name | email | uid | pwd | nickname | phone | address | birth | banknum | bank | grade |
+---------+------+---------------+------+--------------------------------------------------------------+----------+---------------+---------+------------+---------+------+-------+
| 1 | test | test@test.com | test | $2b$10$EMI6bBM/r9x8DQOyz24qieGJxc8vE3.wi8/IC6mbHJ42kMcr2QKFu | test | 010-1111-1111 | N/A | 2024-01-01 | N/A | N/A | 0 |
+---------+------+---------------+------+--------------------------------------------------------------+----------+---------------+---------+------------+---------+------+-------+
1 row in set (0.00 sec)
비밀번호 암호화도 잘 되었고 성공적으로 데이터가 입력된걸 확인할 수 있습니다.
이제 마지막인 응답 메세지만 띄워주도록 하겠습니다.
메세지를 띄우는 방법은 간단합니다. 서버에서 받아온 응답을 미리 만들어둔 msg에 저장하고 alert만 띄워주면 됩니다.
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
axios
.post('http://localhost:3099/api/user/register', formData)
.then(res => {
setMsg(res.data.msg); // 서버에서 응답받은 json의 msg를 저장
})
.catch(e => {
console.log(e);
});
}
backend/src/controller/userController.ts
를 확인해보면
json으로 응답하고 있습니다.
다시 화면으로 돌아와서 useEffect
를 작성합니다.
useEffect(() => {
if (msg !== '') { alert(msg) }
}, [msg])
간단한 useEffect
hook 사용입니다.
msg가 비어있지 않으면 알람을 띄우고, msg가 바뀔때마다 작동합니다.
이제 메세지를 잘 띄우는지 테스트를 해보겠습니다.
테스트 데이터를 채워주고 가입을 해보겠습니다.
mysql> mysql> select * from user;
+---------+-------+----------------+-------+--------------------------------------------------------------+----------+---------------+---------+------------+---------+------+-------+
| user_id | name | email | uid | pwd | nickname | phone | address | birth | banknum | bank | grade |
+---------+-------+----------------+-------+--------------------------------------------------------------+----------+---------------+---------+------------+---------+------+-------+
| 1 | test | test@test.com | test | $2b$10$EMI6bBM/r9x8DQOyz24qieGJxc8vE3.wi8/IC6mbHJ42kMcr2QKFu | test | 010-1111-1111 | N/A | 2024-01-01 | N/A | N/A | 0 |
| 2 | test2 | test2@test.com | test2 | $2b$10$ZSodDFSVrELLlTaw5Hghye4DoyIPjURaSwjz.rF/PANo/k11qHTZe | test2 | 010-2222-2222 | N/A | 2023-12-31 | N/A | N/A | 0 |
+---------+-------+----------------+-------+--------------------------------------------------------------+----------+---------------+---------+------------+---------+------+-------+
2 rows in set (0.00 sec)
데이터베이스에도 입력이 잘되고 메세지도 출력됩니다.
백엔드와 프론트엔드 코드를 한 포스팅에 쓰려니 글이 굉장히 길어졌습니다.
이번 포스팅은 여기서 마치겠습니다.
읽어주셔서 감사합니다.