훌륭한 Nest.js 블로그 만들기: 사용자 시스템 추가
Wenhao Wang
Dev Intern · Leapcell

이전 튜토리얼에서 기사 작성을 지원하는 기본 블로그를 구축했습니다.
하지만 이 블로그에는 인증 기능이 없어서 누구나 기사를 작성하거나 삭제할 수 있다는 위험이 있습니다.
다음 튜토리얼에서는 사용자 및 인증 시스템을 점진적으로 추가하여 이 블로그를 더욱 안전하게 만들 것입니다.
더 이상 지체하지 말고 시작하겠습니다.
인증 방법 소개
웹 개발에서 가장 일반적인 두 가지 인증 방법은 **토큰 기반(예: JWT)**과 **세션 기반(쿠키)**입니다.
- JWT(JSON Web Tokens): 오늘날 가장 인기 있는 인증 방법입니다. 사용자가 로그인하면 서버는 토큰을 생성하여 클라이언트에 반환합니다. 클라이언트는 후속 요청에 이 토큰을 포함하고, 서버는 토큰이 변조되지 않았는지 확인하기만 하면 됩니다. 서버는 사용자의 로그인 상태를 저장하지 않기 때문에, 이 방법은 수많은 서버가 있는 현대의 대규모 웹사이트에 매우 적합하며, 여러 서버 간 사용자의 상태를 자주 동기화할 필요가 없습니다.
- 세션-쿠키: 사용자가 로그인하면 서버는 세션을 생성하고 쿠키를 통해 브라우저에 세션 ID를 반환합니다. 브라우저는 후속 요청에 이 쿠키를 자동으로 포함합니다. 서버는 쿠키의 세션 ID를 사용하여 저장된 해당 세션 정보를 찾아 사용자를 식별합니다.
이 튜토리얼에서는 세션-쿠키 방식을 선택합니다. 저희 블로그는 전통적인 아키텍처를 가진 간단한 애플리케이션입니다. 인증에 세션-쿠키를 사용하는 것은 가장 직접적이고 고전적이며 안전한 접근 방식입니다. Nest.js는 또한 이를 위한 훌륭한 내장 지원을 제공합니다.
사용자 모듈 생성
인증을 다루기 전에 먼저 사용자 시스템을 추가하겠습니다.
이전 튜토리얼에서 posts
모듈을 생성한 것과 유사하게 Nest CLI를 사용하여 필요한 파일을 빠르게 생성합니다.
nest generate module users nest generate controller users nest generate service users
다음으로, 데이터베이스의 users
테이블을 매핑하기 위해 src/users
디렉토리에 사용자 엔티티 파일 user.entity.ts
를 생성합니다.
// src/users/user.entity.ts import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) username: string; @Column() password: string; // 저장되는 비밀번호는 암호화됩니다. }
UsersModule
(src/users/users.module.ts
)에서 TypeOrmModule
을 등록하여 User
엔티티를 작업할 수 있도록 합니다. 나중에 (Auth 모듈과 같이) 다른 모듈에서 사용할 수 있도록 UsersService
를 내보내야 합니다.
// src/users/users.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { User } from './user.entity'; @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [UsersService], exports: [UsersService], // 다른 모듈에서 사용하기 위해 UsersService 내보내기 }) export class UsersModule {}
마지막으로, 이전 튜토리얼에서 생성한 PostgreSQL 데이터베이스에서 다음 SQL 문을 실행하여 user
테이블을 생성합니다.
CREATE TABLE "user" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "username" VARCHAR UNIQUE NOT NULL, "password" VARCHAR NOT NULL );
Leapcell에서 데이터베이스를 생성했다면,
그래픽 인터페이스를 사용하여 SQL 문을 쉽게 실행할 수 있습니다. 웹사이트의 데이터베이스 관리 페이지로 이동하여 SQL 인터페이스에 위의 문을 붙여넣고 실행하기만 하면 됩니다.
사용자 등록 구현
src/users/users.service.ts
를 수정하여 사용자를 생성하고 찾는 로직을 추가합니다.
참고: 보안을 위해 사용자 비밀번호는 데이터베이스에 저장되기 전에 암호화해야 합니다. 비밀번호 암호화에는 bcrypt
라이브러리를 사용합니다.
필요한 종속성을 설치합니다.
npm install bcrypt npm install -D @types/bcrypt
// 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 usersRepository: Repository<User> ) {} async create(user: Partial<User>): Promise<User> { const saltRounds = 10; const hashedPassword = await bcrypt.hash(user.password || '', saltRounds); const newUser = this.usersRepository.create({ username: user.username, password: hashedPassword, }); return this.usersRepository.save(newUser); } async findOne(username: string): Promise<User | null> { return this.usersRepository.findOneBy({ username }); } }
인증 모듈 생성 및 로그인 로직 검증
모든 인증 관련 로직은 별도의 auth
모듈에 배치할 것입니다. 먼저 핵심 "사용자 검증" 로직만 구현합니다.
CLI를 사용하여 auth
모듈과 service
를 생성합니다.
nest generate module auth nest generate service auth
src/auth/auth.service.ts
를 수정하여 사용자 이름과 비밀번호가 올바른지 확인하는 validateUser
메서드를 추가합니다.
// src/auth/auth.service.ts import { Injectable } from '@nestjs/common'; import { UsersService } from '../users/users.service'; import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { constructor(private usersService: UsersService) {} async validateUser(username: string, pass: string): Promise<any> { const user = await this.usersService.findOne(username); if (user && (await bcrypt.compare(pass, user.password))) { const { password, ...result } = user; return result; // 검증 성공, 비밀번호 없이 사용자 정보 반환 } return null; // 검증 실패 } }
이제 AuthModule
에 UsersModule
을 가져와 AuthService
가 UsersService
를 사용할 수 있도록 합니다.
// src/auth/auth.module.ts import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { UsersModule } from '../users/users.module'; @Module({ imports: [UsersModule], controllers: [AuthController], providers: [AuthService], }) export class AuthModule {}
로그인 및 회원가입 페이지 생성
사용자 상호작용을 위한 인터페이스를 제공해야 합니다. views
폴더에서 login.ejs
와 register.ejs
를 생성합니다.
register.ejs
<%- include('_header', { title: 'Register' }) %> <form action="/users/register" method="POST" class="post-form"> <h2>Register</h2> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" name="username" required /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" name="password" required /> </div> <button type="submit">Register</button> </form> <%- include('_footer') %>
login.ejs
<%- include('_header', { title: 'Login' }) %> <form action="/auth/login" method="POST" class="post-form"> <h2>Login</h2> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" name="username" required /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" name="password" required /> </div> <button type="submit">Login</button> </form> <%- include('_footer') %>
라우팅 및 컨트롤러 로직 구현
이제 등록 및 로그인 요청을 처리하는 라우트를 생성합니다.
등록 라우트
src/users/users.controller.ts
를 업데이트하여 등록 페이지 및 폼 제출을 처리합니다.
// src/users/users.controller.ts import { Controller, Get, Post, Render, Body, Res } from '@nestjs/common'; import { UsersService } from './users.service'; import { Response } from 'express'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get('register') @Render('register') showRegisterForm() { return; } @Post('register') async register(@Body() body: any, @Res() res: Response) { // 단순화를 위해 복잡한 유효성 검사는 없음 await this.usersService.create(body); res.redirect('/auth/login'); // 등록 성공 후 로그인 페이지로 리디렉션 } }
로그인 검증 라우트
로그인 요청을 처리할 새 auth.controller.ts
를 생성합니다.
nest generate controller auth
src/auth/auth.controller.ts
를 편집합니다. 여기서 validateUser
메서드를 호출합니다. 검증이 성공하면 블로그 홈페이지로 리디렉션합니다.
실패할 경우, 단순함을 위해 지금은 특별히 처리하지 않겠습니다. 실제 애플리케이션에서는 일반적으로 오류 메시지를 반환합니다.
// src/auth/auth.controller.ts import { Controller, Get, Post, Render, Body, Res, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; import { Response } from 'express'; @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} @Get('login') @Render('login') showLoginForm() { return; } @Post('login') async login(@Body() body: any, @Res() res: Response) { const user = await this.authService.validateUser(body.username, body.password); if (!user) { throw new UnauthorizedException(); } // 검증 성공 res.redirect('/posts'); } }
마지막으로 app.module.ts
에 UsersModule
과 AuthModule
을 가져옵니다.
// src/app.module.ts // ... imports import { UsersModule } from './users/users.module'; import { AuthModule } from './auth/auth.module'; @Module({ imports: [ // ... TypeOrmModule.forRoot(...) PostsModule, UsersModule, AuthModule, ], // ... }) export class AppModule {}
이 시점에서 기본 사용자 등록 시스템과 로그인 로직 검증을 완료했습니다. 사용자는 계정을 만들 수 있으며 애플리케이션은 그들의 신원을 확인할 수 있습니다.
테스트해 봅시다. 프로젝트를 시작합니다.
npm run start:dev
등록하려면 http://localhost:3000/users/register
를 방문하세요.
등록 성공 후 자동으로 http://localhost:3000/auth/login
으로 리디렉션되어 로그인합니다.
올바른 계정 정보와 잘못된 계정 정보를 입력하는 결과를 테스트할 수 있습니다. 예를 들어 잘못된 정보를 입력하면 401 Unauthorized 오류 페이지가 표시됩니다.
하지만 현재 로그인은 단순히 일회성 프로세스로, 로그인 인증 흐름을 경험하기 위한 것이며 서버는 사용자의 로그인 상태를 기억하지 않습니다.
다음 기사에서는 실제 사용자 세션 지속성을 달성하고 사용자 권한에 따라 페이지 및 기능에 대한 접근을 제한하기 위해 로그인 인증 로직을 완료할 것입니다.
이전 튜토리얼: