쇼핑몰 프로젝트 - 2. 회원가입(Express, Typescript, Next.js, typeorm, mySQL )

Bumgu·2024년 1월 10일
0
post-thumbnail

1. 백엔드 코드 작성

1-1 DB연결

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을 다운받아줍니다.

  • dotenv : env파일에서 db정보를 가져와 변수로 저장 코드에 드러나면 보안에 문제가 되기때문에 사용
  • typeorm : orm라이브러리
  • reflect-metadata : 데코레이터와 관련한 메타데이터와 상호작용하는 수단을 제공

이후 src/config디렉토리에 db.ts파일을 생성합니다.

  • 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

1-2 router 작성

  • app.ts
    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로 라우팅 됩니다.

  • routes/userRouter.ts
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를 실행합니다.

1-3 user entity 작성

  • entity/User.ts
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을 사용합니다.

1-4 db config 수정

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에 추가합니다.

1-5 user model 작성

  • model/userModel.ts
export interface UserInterface {
	name: string;
	email: string;
	uid: string;
	pwd: string;
	nickname: string;
	phone: string;
	address: string;
	birth: Date;
	banknum: string;
	bank: string;
}

이렇게 사용자 정보를 하나의 인터페이스로 정의함으로써 코드에서 사용자 객체를 다룰 때 일관된 구조를 가질 수 있습니다. 또한 TypeScript의 정적 타입 검사 기능을 활용하여 코드의 안정성을 높일 수 있습니다. 수정이 생긴다면 이 부분만 고쳐주면 되어서 인터페이스를 사용하면 유지보수성을 향상시킬 수 있습니다.

1-6 controller 작성

  • controller/userController.ts
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: '회원가입 실패' });
	}
}

req: Request<{}, {}, UserInterface>

이 부분은 Express에서 제공되는 Request 객체의 타입을 지정하는 부분입니다.

  • Request<{}, {}, UserInterface>: 이것은 TypeScript의 제네릭을 사용하여 Request 객체의 타입을 지정하는 부분입니다.

    1. 첫 번째 제네릭 ({}): HTTP 요청의 파라미터(쿼리스트링, 루트 매개변수 등)에 대한 타입을 나타냅니다. 여기서는 비어있는 객체 {}로 표시되어 있으므로, 요청 파라미터의 타입이 없음을 의미합니다.

    2. 두 번째 제네릭 ({}): HTTP 요청의 바디에 대한 타입을 나타냅니다. 여기서도 비어있는 객체 {}로 표시되어 있으므로, 요청 바디의 타입이 없음을 의미합니다.

    3. 세 번째 제네릭 (UserInterface): HTTP 요청에 포함되는 사용자 정보에 대한 타입을 나타냅니다. 이 부분이 중요한데 UserInterface는 위에서 작성한 model/userModel.ts에서 정의한 사용자 정보를 담고 있는 TypeScript 인터페이스입니다. 이를 통해 요청 바디가 예상대로 사용자 정보를 포함하고 있음을 명시합니다.

즉, Request 객체가 빈 쿼리스트링과 빈 바디를 가지며, 사용자 정보는 UserInterface에 정의된 형식을 따라야 한다고 명시한 것입니다.

이후 try-catch 문으로 감싸주고 registerServicereq.body를 매개변수로 담아주고 그 결과를 userData에 저장해서 출력해 봅니다. 이후 Response객체에 json 형태로 회원가입이 완료되었다는 응답을 합니다.

1-7 service 작성

  • service/userService.ts
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);

  • AppDataSource : 데이터베이스 설정에 관련된 정보를 가지고 있습니다.(db.ts)
  • getRepository(User) : typeorm에서 사용하는 데이터베이스와 연결된 repository를 얻을 수 있습니다. 데이터베이스의 레코드와 매핑되는 개체는 미리 만든 User entity를 사용합니다.

비밀번호 암호화를 할 것이기 때문에 yarn add bcrypt @types/bcrypt 명령어로 bcrypt를 설치합니다. 암호화에 대한 내용은 다음 포스팅에서 다루겠습니다.
controller에서 넘어온 유저 정보를 객체 분해 할당(Object Destructuring Assignment) 으로 각 변수에 할당합니다.
grade는 유저가 정하는 것이 아니기 때문에 0으로 초기화 했습니다.
이후 그 변수들을
repository.create() 함수 안에 담아서 데이터를 생성합니다. 이정보는 User entity에 맞춰서 생성됩니다.
repository.save() 함수로 저장되고, 이에 대한 정보를 userData변수에 저장하고 controller에 반환합니다.

이제 백엔드는 완료되었으니 화면을 만들어보겠습니다.

2. 프론트엔드 코드 작성

회원가입 화면을 생성하겠습니다 위치는 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를 렌더링 합니다.

2-1 회원가입 화면 생성

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를 만들어도 되지만, 이렇게 객체로 생성하는것이 효과적입니다.

2-2 onSubmit 작성

현재코드는 버튼을 아무리 눌러도 아무런 변화가 일어나지 않습니다. 그렇기 때문에 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}>

2-3 onChange 작성

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 속성에 추가합니다.

3. 테스트

여기까지 했다면 회원가입 기능은 작동하고 데이터베이스에 추가될 것입니다. 확인해보겠습니다.

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)

비밀번호 암호화도 잘 되었고 성공적으로 데이터가 입력된걸 확인할 수 있습니다.

4. 알람띄우기

이제 마지막인 응답 메세지만 띄워주도록 하겠습니다.
메세지를 띄우는 방법은 간단합니다. 서버에서 받아온 응답을 미리 만들어둔 msg에 저장하고 alert만 띄워주면 됩니다.

  • onSubmit 함수
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가 바뀔때마다 작동합니다.

5. 결과

이제 메세지를 잘 띄우는지 테스트를 해보겠습니다.

테스트 데이터를 채워주고 가입을 해보겠습니다.

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)

데이터베이스에도 입력이 잘되고 메세지도 출력됩니다.


백엔드와 프론트엔드 코드를 한 포스팅에 쓰려니 글이 굉장히 길어졌습니다.
이번 포스팅은 여기서 마치겠습니다.
읽어주셔서 감사합니다.

profile
Software VS Me

0개의 댓글