Search

12. 앱 구성 관리하기

NestJS에서 개발과 테스트 환경을 나눈 건 정말 중요한 작업이다.
이번 글에서는 이걸 어떻게 해낼 수 있을지, 그리고 ConfigService를 어떻게 활용할지 차근차근 알아보자.

이전 글 복습

AppModule을 열어보면 TypeOrmModule이 있다.
이 안에서 DB 연결 정보를 설정하고 있는데 보통은 여기에 db.sqlite 같은 파일명을 직접 써넣는다.
그런데 개발과 테스트에 따라 다른 DB를 써야한다면?
TypeOrmModule.forRoot({ type: 'sqlite', database: process.env.NODE_ENV === 'test' ? 'test.sqlite' : 'db.sqlite', ... })
TypeScript
복사
이런 식으로 삼항 연산자로 해결할 수도 있다.
간단하고 깔끔하지만 우리는 NestJS에서 권장하는 방식을 따를 것이다.

환경 변수 처리, Nest 방식대로 해봅시다

1. @nestjs/config 패키지 설치하기

우선 Nest에서는 @nestjs/config를 통해 환경 변수를 설정하는 걸 권장한다.
그래서 먼저 패키지를 설치한다.
npm install @nestjs/config
Bash
복사
설치 후에는 서버를 다시 시작해준다.
npm run start:dev
TypeScript
복사
이 패키지 안에는 유명한 dotenv 라이브러리가 함께 들어있다. 이 라이브러리는 다음 두 가지 출처에서 환경 변수를 가져온다.
1.
시스템 환경 변수 (예: export DB_PASSWORD=abc)
2.
.env 파일
이걸 하나의 객체로 결합해서, 애플리케이션 전체에서 사용할 수 있게 해준다.
Nest에서는 이 객체를 ConfigService를 통해 사용할 수 있다.
그런데 왜 dotenv를 직접 안 쓰고 Nest Config를 쓸까? dotenv.config() 한 줄이면 끝날 일을 굳이 복잡하게 ConfigService로 구현하는 이유가 뭘까? Nest의 방식이 복잡하긴 하지만, DI(의존성 주입) 덕분에 유지보수와 테스트가 쉬워지고, 모듈 단위로 분리도 깔끔하게 된다. 개인적으로는 과하다고 느끼지만, Nest 생태계에서 일관성 있게 관리하려면 이게 가장 좋은 방법이다.

2. .env 파일 만들기

먼저 프로젝트 루트 디렉터리에 다음 파일들을 생성한다.
.env.development → 개발 환경 설정
DB_NAME=db.sqlite
TypeScript
복사
.env.test → 테스트 환경 설정
DB_NAME=test.sqlite
TypeScript
복사
각 파일마다 다른 DB 이름을 사용해 테스트 환경과 개발 환경을 분리한다.

3. ConfigModule과 ConfigService 연결하기

이제 @nestjs/config 패키지를 통해 이 파일들을 실제 애플리케이션에 연결해보자.
먼저 app.module.ts 파일 상단에 import를 추가한다.
import { ConfigModule, ConfigService } from '@nestjs/config';
TypeScript
복사
그리고 imports 배열에 아래와 같이 ConfigModule을 추가한다.
ConfigModule.forRoot({ isGlobal: true, envFilePath: `.env.${process.env.NODE_ENV}`, }),
TypeScript
복사
isGlobal: true: 전체 애플리케이션에서 이 모듈을 공통으로 사용하도록 설정
envFilePath: 현재 환경(NODE_ENV)에 따라 알맞은 .env 파일을 불러오도록 설정
예를 들어, NODE_ENV=development일 경우 .env.development 파일을 읽는다.

4. TypeORM 설정에 ConfigService 주입하기

이제 TypeORM 설정에 .env의 내용을 반영해보자.
이때 주의할 점은 단순히 configService.DB_NAME 이런 식으로 접근할 수는 없고, 의존성 주입이 필요하다.
TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => { return { type: 'sqlite', database: config.get<string>('DB_NAME'), synchronize: true, entities: [User, Report], }; }, }),
TypeScript
복사
useFactory: TypeORM 설정을 함수로 반환하며, 이 안에서 ConfigService를 사용할 수 있음
config.get<string>('DB_NAME'): 환경 파일에서 DB_NAME 값을 가져옴

5. NODE_ENV 설정 해주기

가장 중요한 마지막 단계가 남았다.
바로 앱을 실행할 때 NODE_ENV 값을 명확히 설정하는 것이다.
만약 NODE_ENV가 설정되지 않으면 .env.undefined 같은 존재하지 않는 파일을 읽으려 할 수 있다.
이 경우 앱이 곧바로 에러를 내지 않기 때문에 나중에 디버깅이 훨씬 어려워진다.
먼저, package.json 파일을 연다.
"scripts": { "start": "cross-env NODE_ENV=development nest start", "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", "test": "cross-env NODE_ENV=test jest", "test:watch": "cross-env NODE_ENV=test jest --watch --maxWorkers=1", "test:cov": "cross-env NODE_ENV=test jest --coverage", "test:debug": "cross-env NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "cross-env NODE_ENV=test jest --config ./test/jest-e2e.json" },
JSON
복사
이제 각 실행 스크립트에 환경 변수를 설정한다.
Linux나 Mac에서는 보통 아래처럼 환경 변수를 붙이면 되지만, Windows에서는 작동하지 않기 때문에 cross-env 라는 도구를 사용한다.
NODE_ENV=development nest start --watch
Bash
복사
터미널에서 다음 명령어를 실행한다.
npm install --save-dev cross-env
Bash
복사
모든 스크립트에 cross-env를 적용한다.
"scripts": { "start": "cross-env NODE_ENV=development node dist/main", "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "cross-env NODE_ENV=development nest start --debug --watch" "test": "cross-env NODE_ENV=test jest", "test:watch": "cross-env NODE_ENV=test jest --watch", "test:cov": "cross-env NODE_ENV=test jest --coverage", "test:debug": "cross-env NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "cross-env NODE_ENV=test jest --config ./test/jest-e2e.json" },
JSON
복사

6. 테스트 실행 확인

환경 구성이 완료됐으니, 이제 실행해보자.

개발 환경 실행

npm run start:dev
JSON
복사
애플리케이션이 정상적으로 실행되고, API 요청 시 기존 사용자 이메일도 검증하는 로직도 잘 작동하는 걸 확인했다.

테스트 실행

npm run test:e2e
JSON
복사
처음 실행 시 데이터베이스 잠김 현상(SQLITE_BUSY: database is locked)이 있었지만, 테스트 자체는 정상적으로 작동한다.
이 오류는 SQLite에서 동시에 여러 연결을 시도할 때 자주 발생하는 문제이다.
Jest는 여러 테스트 파일(.spec.ts)을 동시에 실행하려고 하기 때문에, 하나의 SQLite 데이터베이스(test.sqlite)에 여러 인스턴스가 동시에 접근하게 되어 충돌이 일어난다.
SQLite는 다중 연결을 잘 지원하지 않으며, 하나의 연결만 허용하는 것이 일반적이다.
따라서 병렬 실행이 아닌, 한 번에 하나의 테스트 파일만 실행되도록 설정하면 이 문제를 해결할 수 있다.
생각보다 이 방법은 속도 저하 없이 더 안정적이다.
package.json 파일의 test:e2e 스크립트를 다음과 같이 수정한다.
"test:e2e": "cross-env NODE_ENV=test jest --config ./test/jest-e2e.json --maxWorkers=1"
JSON
복사
-maxWorkers=1: Jest가 테스트 파일을 한 번에 하나씩 실행하도록 지정합니다.
기존에 생성된 test.sqlite 파일을 삭제한 다음 다시 테스트를 실행해보면 정상적으로 동작하는 것을 볼 수 있다.
npm run test:e2e
JSON
복사

7. SQLite 초기화하기

마지막으로 추가해줄 작업은 테스트가 완료된 다음 데이터베이스를 초기화하여 다음 테스트에 영향을 미치지 않도록 하는 것이다.
데이터베이스를 초기화 하는 방법은 크게 2가지가 있다.
1.
각 테이블의 데이터를 DELETE FROM 쿼리로 제거하는 방법
2.
test.sqlite 파일을 지우고, 테스트가 시작되면 TypeORM이 새로 생성하게 하는 방법
우리는 2번 방법으로 SQLite를 초기화해보자.
어떻게 하면 구현할 수 있을까?
모든 테스트 파일마다 beforeEach()test.sqlite 삭제 코드를 넣는 건 비효율적이다.
대신 전역 beforeEach()를 설정해서 모든 테스트 전에 한 번씩 자동으로 실행되도록 만들 수 있다.
test/jest-e2e.json 파일에 아래 내용을 추가한다.
{ "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "moduleNameMapper": { "^src/(.*)$": "<rootDir>/../src/$1" }, "setupFilesAfterEnv": ["<rootDir>/setup.ts"] }
JSON
복사
setupFilesAfterEnv: 모든 테스트 실행 전에 특정 파일을 실행하도록 지정하는 옵션
test 디렉토리에 setup.ts 파일을 만들고 아래 내용을 작성한다.
import { rm } from 'fs/promises'; import { join } from 'path'; global.beforeEach(async () => { try { await rm(join(__dirname, '..', 'test.sqlite')); } catch (err) { // 파일이 없을 수도 있으니 에러는 무시 } });
TypeScript
복사
fs/promises의 rm을 사용해 파일 삭제
path.join(__dirname, '..', 'test.sqlite')는 test 디렉토리 상위 경로에 있는 test.sqlite 파일을 가리킵니다
파일이 없는 경우도 있으므로 try-catch로 감싸 오류를 무시합니다
터미널에서 테스트를 해보면 반복 실행 시에도 모든 테스트가 성공하는 것을 확인할 수 있다.

8. .env 파일 Git 커밋 방지 설정

환경 설정 파일은 절대 Github에 올라가서는 안된다.
비공개 정보(비밀번호, DB 주소 등)가 담겨 있기 때문이다.
.gitignore 파일을 열고 다음 두 줄을 추가한다.
.env.development .env.test
JSON
복사
이제 개발용/테스트용 .env 파일은 git 커밋 대상에서 제외된다.

인증 로직 테스트하기 (whoAmI)

다음은 인증 로직이 잘 작동하는지 확인하는 테스트이다.
목표는 사용자 가입 후, 해당 사용자가 로그인되어 있는지 확인하는 것이다.

테스트 시나리오

1.
사용자를 /auth/signup으로 가입시킴
2.
응답 쿠키를 통해 로그인 상태를 유지
3.
/auth/whoami 요청을 보내 로그인된 사용자 정보 확인
it('signup as a new user then get the currently logged in user', async () => { const email = 'test@example.com'; const res = await request(app.getHttpServer()) .post('/auth/signup') .send({ email, password: 'password' }) .expect(201); const cookie = res.get('Set-Cookie'); const { body } = await request(app.getHttpServer()) .get('/auth/whoami') .set('Cookie', cookie) .expect(200); expect(body.email).toEqual(email); });
TypeScript
복사
res.get(’Set-Cookie’)를 통해 받은 쿠키를 후속 요청에 헤더로 전달해, 로그인 상태를 시뮬레이션한다.