Nest.js 얼렁뚱땅 게시판 #3
가능한 이해에 들이는 시간을 줄이고 구현을 어서 끝내는데 의의를 두려고 했다. 이대로 가다간 정말 타이핑과 페이지 새로고침만 연속하게 될 것 같아서 한 템포 끊는 시간을 반드시 가져야 할 것 같다.
2025.06.16 - [with Nest] - Nest.js 얼렁뚱땅 게시판 #2
Nest.js 얼렁뚱땅 게시판 #2
웹을 만들건, 앱을 만들건, 그 본질이 크게 다르지 않다. 여기서 이야기하고 싶은 것은 단어 몇 개 다르다고 쫄면 안된다는 것인데, 아.. 잘 모르겠다. 내가 실제 돈 벌어오는 서비스를 만들어본
hyeonistic.tistory.com
우린 CRUD의 기본을 구현해냈다. 이어서 나는 다음과 같은 것들을 구현 목표로 만들었다 :
- 로그인 시스템을 갖춘다.
- 프론트 영역의 내용들과 연결시킨다.
인증 모듈 설치가 필요해서 설치하였다. 당장 얘네가 뭘 하는지에 대해선 그렇게 크게 개의치 않기로 했다.
cd [백엔드 프로젝트 이름]
npm install @nestjs/jwt @nestjs/passport passport passport-local bcrypt
npm install -D @types/passport-local @types/bcrypt
[위 내용들 설치가 완료되면 곧바로 인증 모듈 생성 하는 커맨드들을 입력한다.]
nest g module auth
nest g service auth
nest g controller auth
nest g module users
nest g service users
유저의 기본 정보를 생성했다. src/users/user.entity.ts 이번에도 TypeORM을 사용한다.
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
name: string;
@Column()
password: string;
@CreateDateColumn()
createdAt: Date;
}
유저 서비스를 구현해야 한다. src/users/users.service.ts
코드 내용을 전체적으로 보면 알 수 있는 것이 실제 작동하는 루틴과 도달시킬 수 있는 포인트와 연결시킨다는 느낌이다.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async create(email: string, name: string, password: string): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 10);
const user = this.userRepository.create({
email,
name,
password: hashedPassword,
});
return await this.userRepository.save(user);
}
async findByEmail(email: string): Promise<User | null> {
return await this.userRepository.findOne({ where: { email } });
}
async validatePassword(user: User, password: string): Promise<boolean> {
return await bcrypt.compare(password, user.password);
}
}
좋다. 이제 인증 서비스 부분도 구현을 해야한다. src/auth/auth.service.ts
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async signup(email: string, name: string, password: string) {
const existingUser = await this.usersService.findByEmail(email);
if (existingUser) {
throw new BadRequestException('이미 사용 중인 이메일입니다');
}
const user = await this.usersService.create(email, name, password);
const payload = { sub: user.id, email: user.email, name: user.name };
return {
access_token: await this.jwtService.signAsync(payload),
user: { id: user.id, email: user.email, name: user.name },
};
}
async login(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user || !(await this.usersService.validatePassword(user, password))) {
throw new UnauthorizedException('이메일 또는 비밀번호가 틀렸습니다');
}
const payload = { sub: user.id, email: user.email, name: user.name };
return {
access_token: await this.jwtService.signAsync(payload),
user: { id: user.id, email: user.email, name: user.name },
};
}
}
DTO에 대한 코드를 작성 하겠다. 차례대로, src/auth/dto/signup.dto.ts 그리고 같은 경로에서 login.dto.ts 이다.
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
export class SignupDto {
@IsEmail()
email: string;
@IsNotEmpty()
name: string;
@IsNotEmpty()
@MinLength(6)
password: string;
}
import { IsEmail, IsNotEmpty } from 'class-validator';
export class LoginDto {
@IsEmail()
email: string;
@IsNotEmpty()
password: string;
}
마지막으로 인증에 관한 Controller를 구현하자. src/auth/auth.controller.ts 에 작성한다.
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignupDto } from './dto/signup.dto';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('signup')
async signup(@Body() signupDto: SignupDto) {
return await this.authService.signup(
signupDto.email,
signupDto.name,
signupDto.password,
);
}
@Post('login')
async login(@Body() loginDto: LoginDto) {
return await this.authService.login(loginDto.email, loginDto.password);
}
}
이 진도쯤 내가 정리한 것을 따로 정리 할 곳이 없어 그냥 여기 끝에 작성하기로 했다. 굵은 글씨는 질의, 그렇지 않은건 내 검색 결과이다.
백과 프론트의 교신은 생각보다 간단하게 이루어지는 것 같다. 어쨌든 각기의 주체는 이미 주어지는 숫자에 예를 들면 포트 3000, 5173으로 주어지는데, 프론트도 처음 초기화 될 때 하드 코딩 내지 매핑으로 이루어지니 실제 전달받을 내용을 이곳에서 주고 받는다는 느낌이다.
axios.get('http://localhost:3000/board')
프론트엔드에서의 버튼 클릭으로 인해 백엔드를 향해 요청이 보내지고, 백엔드가 처리 후 응답을 반환하는 요청-응답 패턴이 기본이다.
표현을 포트 번호 두개로 이야기 하긴 했지만, 실제 서비스에서는
https://website.com 그리고 https://api.website.com 이런 식으로 구분해서 프론트와 백을 놓는다는 듯.
C언어에서는 어떤 토픽 내지 키워드를 다루는데 있어 .c, .h 파일 두 개만 보면 된다. 예를 들어 기존에 했던 PintOS만 봐도 list.c, list.h가 있다시피.. 다만 Nest.js이기에 그런건지, 모든 typescript가 주된 인프라들이 이런 것인지, 한 토픽에 대한 아이템이 정말 많다. controller.spec, controller, module, service. 이렇게 파일을 찢는 형태의 이해가 쉽지만은 않다.
// controller.ts - API 엔드포인트만
// service.ts - 비즈니스 로직만
// module.ts - 의존성 연결만
// spec.ts - 테스트만
이렇게 분리하는 이유는 Controller는 요청만 받고, Service는 비즈니스 로직만 처리하는데 특화시키는 코드라고 한다. 이렇게 함으로써 유지보수성과 테스트 용이성을 확보한다는데, 나는 이게 확 와닿지가 않는다.
어쨌든 다 떠나서 이것은 웹 페이지이다. 즉 가만히 두면 아무것도 작동하지 않는 개념이다. 모든 함수는 트리거 성이 강하다. 그럼 상시 작동하는 개념이 아예 없는건 아닐텐데 무엇무엇이 있을까.
웹은 Event-driven Communication 기반이다. 특정한 버튼을 배치 할 때 onclick={() => handleLogin()} 와 같은 내용을 배치했었다. 그럼에도 상시 작동하는 것들도 있다 :
백엔드 서버
await app.listen(3000);
socket.io 실시간 통신
socket.on('message', (data) => {
// 메시지가 오면 즉시 처리
});
그냥 주기적 돌리기
setInterval(() => {
fetchLatestData();
}, 5000);
물론 흔한 방법은 그냥 페이지에 특정한 아이템에 대해 새로고침 버튼을 배치하는 것이다.