
유저 인증 기능을 구현하는 방식에는 여러 가지가 있지만, 그중에서도 나는 JWT 토큰을 발급해 쿠키로 내려주는 방식을 선호한다. 이유는 여럿 있겠지만, 가장 큰 이유는 클라이언트 측에서 별도의 처리를 거의 하지 않아도 된다는 점이다. 토큰 저장 방식이나 갱신 전략에 대해 프론트엔드가 신경 쓸 필요가 없고, 인증과 관련된 모든 흐름을 백엔드에서 일관되게 통제할 수 있다.
물론 이 방식에도 단점은 존재한다. 하지만 백엔드가 인증과 토큰 관리에 대한 전권을 가지는 구조에서는, 그러한 단점들이 충분히 상쇄된다고 생각한다. 특히 보안과 유지보수 측면에서는 이점이 더 분명하다.
Nest.js에서는 @nestjs/jwt 패키지를 사용해 JWT를 발급하고 검증한다. 이 패키지는 JwtModule과 JwtService를 제공하며, 이를 통해 토큰 생성과 검증 로직을 비교적 간단하게 구성할 수 있다. JwtModule은 동적 모듈이기 때문에 register 메서드를 호출해 생성한다. 다양한 옵션을 전달할 수 있지만, 나는 토큰의 secret과 만료 시간 등을 직접 제어하기 위해 별도의 옵션 없이 모듈을 등록하는 편이다.
import { JwtModule } from '@nestjs/jwt';
@Module({
controllers: [AuthController],
providers: [AuthService, AuthRepository],
imports: [JwtModule.register({})],
})
export class AuthModule {}JwtService는 일반적으로 서비스 레이어에서 주입받아 사용한다.
import { JwtService } from '@nestjs/jwt';
import { jwtConfig } from 'src/config/jwt.config';
import { ConfigType } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
@Inject(jwtConfig.KEY) private jwt: ConfigType<typeof jwtConfig>,
private authRepository: AuthRepository,
private jwtService: JwtService,
) {}
}JwtService는 크게 두 가지 핵심 메서드를 제공한다. 하나는 payload를 JWT로 생성하는 sign, 다른 하나는 토큰의 유효성을 검증하는 verify이다.
Express.js에서 흔히 사용하는 jsonwebtoken 패키지와 마찬가지로, verify는 토큰이 유효하면 payload를 반환하고, 문제가 있을 경우 예외를 발생시킨다. Nest.js의 JwtService 역시 이 흐름을 따르며, 토큰이 만료된 경우에는 TokenExpiredError, 토큰 자체가 잘못된 경우에는 JsonWebTokenError를 던진다. 이 예외 기반 흐름은 가드나 인터셉터와 결합하기에도 적합하다
나는 AuthService 내부에 토큰 생성, 검증, 재발급을 담당하는 메서드를 명시적으로 분리해 사용하고 있다.
generateToken(
payload: { email: string; nickname: string },
type: 'access' | 'refresh',
) {
const secret =
type === 'access'
? this.jwt.accessSecret
: this.jwt.refreshSecret;
const expiresIn = type === 'access' ? '1h' : '30d';
return this.jwtService.sign(payload, {
secret,
expiresIn,
});
}verify(token: string, type: 'access' | 'refresh') {
const secret =
type === 'access'
? this.jwt.accessSecret
: this.jwt.refreshSecret;
const { email, nickname } = this.jwtService.verify(token, {
secret,
});
return { email, nickname };
}refresh(refresh: string) {
const { email, nickname } = this.jwtService.verify(refresh, {
secret: this.jwt.refreshSecret,
});
const accessToken = this.generateToken(
{ email, nickname },
'access',
);
const refreshToken = this.generateToken(
{ email, nickname },
'refresh',
);
return { accessToken, refreshToken };
}물론 AuthModule에서 JwtModule을 export하면, 이를 import한 다른 모듈에서도 JwtService를 직접 사용할 수 있다. 혹은 JWT 로직이 필요한 모든 모듈에서 JwtModule을 각각 import하는 방식도 가능하다(이미지 왼쪽).
하지만 나는 유지보수 관점에서 JwtService를 오직 AuthService 내부에서만 사용하도록 제한하는 편이다. 다른 모듈에서는 JwtService 대신 AuthService를 통해서만 토큰 관련 로직에 접근하도록 한다. 이렇게 하면 JWT와 관련된 환경 변수, 토큰 생성 방식, 예외 처리 로직이 모두 AuthService에 응집된다. 결과적으로 인증 로직의 변경 범위가 명확해지고, 여러 모듈에서 JWT를 사용하는 방식도 자연스럽게 통일된다(이미지 오른쪽).

앞서 언급했듯이, 나는 토큰 관리에 대한 권한을 전적으로 백엔드가 가지는 구조를 선호한다. 이를 위해 쿠키에 토큰을 저장할 때는 반드시 httpOnly, secure, sameSite 옵션을 설정해 클라이언트가 토큰에 직접 접근하지 못하도록 해야 한다.
Nest.js에서는 컨트롤러 메서드가 값을 반환하는 방식이 일반적이지만, 쿠키를 설정해야 하는 경우에는 @Res 데코레이터를 사용해 응답 객체를 직접 다루게 된다.
@Post('signup')
async signUp(
@Body() signUpDto: SignUpDto,
@Res() res: Response,
) {
const { accessToken, refreshToken } =
await this.authService.signup(signUpDto);
res
.cookie('accessToken', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
})
.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
})
.status(201)
.send({ message: '회원가입 성공' });
}이렇게 구성하면 클라이언트는 토큰의 존재조차 인식하지 못한 채 인증된 상태를 유지하게 되고, 인증 흐름 전반을 서버 중심으로 안정적으로 관리할 수 있다.
더 읽어보기
2025.05.21
14. Redis를 사용한 세션 관리 및 캐싱
새로운 프로젝트를 준비하면서 인증 방식부터 다시 고민하게 되었다. 이전 프로젝트에서는 JWT를 사용해 유저 인증과 상태 관리를 처리했지만, 이번에는 Redis를 활용한 세션 방식으로 전환하기로 결정했다. 이 방식은 유저 상태를 서버에서 직접 관리할 수 있어 보안 측면에서도 유리하고, 필…
2025.01.30
비즈니스 로직은 어디에 있어야 할까
Nest.js에서 컨트롤러와 각각의 프로바이더는 분명한 책임을 갖는다. 하지만 막상 서버를 개발하다 보면, 이 책임들을 깊이 고민하지 않은 채 코드를 작성하게 되고, 그 결과 클래스들이 서로의 영역을 침범하는 상황이 반복된다. 나 역시 컨트롤러에 비즈니스 로직이 섞이거나, 서비스가 지나…
2024.12.07
13. Jest 테스트 구성
이 포스트를 시작하기에 앞서 한 가지 분명히 밝혀두고 싶은 점이 있다. 나는 컨트롤러(혹은 GraphQL 기준으로는 리졸버) 와 레포지토리 클래스에 대한 테스트를 거의 작성하지 않는다.이유는 단순하다. 애플리케이션에서 가장 많은 비즈니스 규칙이 응집되어 있는 곳은 서비스 계층이며, 테스…
2024.11.14
12. GraphQL로 요청 처리
GraphQL은 페이스북에서 개발한 데이터 쿼리 언어이자 런타임으로, 기존 REST API가 가진 구조적 한계를 보완하기 위해 등장했다. 전통적인 REST API에서는 각 엔드포인트마다 반환되는 데이터 구조가 고정되어 있고, 동일한 리소스에 대해 서로 다른 작업을 수행하기 위해 HTTP…
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
파일을 다운로드할 때 가끔 몇 퍼센트 진행되었는지 혹은 진행 막대(progress bar)가 조금씩 채워지는 모습을 볼 수 있다. 그런데 모든 다운로드가 이런 식으로 진행률을 보여 주는 것은 아니다. 어떤 다운로드는 퍼센트가 표시되지만, 어떤 경우에는 진행 막대 없이 로딩 스피너만 계속…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감을 느끼게 된다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 살펴보고 있는데, 어딘가 낯설지 않다. Java를 써 본 사람이라면 InputStream, OutputStre…
댓글
댓글을 불러오는 중...