Skip to content
← Back to blog
NestJSAngularSaaSKorean가이드튜토리얼

NestJS + Angular로 SaaS 만들기: 완전 가이드

Firas Sayah·July 18, 2026·8 min read

NestJS + Angular: SaaS 개발의 최적 조합

지난해 저는 사이드 프로젝트로 B2B SaaS를 만들기 시작했습니다. 인증, 결제, 멀티테넌시 — 이 세 가지 기본 기능만 구현하는 데 5개월이 걸렸습니다. 실제 비즈니스 로직은 한 줄도 작성하지 못한 채로요. 그때 깨달았습니다: SaaS 개발에서 가장 큰 실수는 모든 것을 처음부터 만들려는 것이라고.

코드가 표시된 모니터 화면

2026년 한국의 SaaS 시장은 빠르게 성장하고 있습니다. 스타트업부터 대기업까지, 클라우드 기반 소프트웨어에 대한 수요가 폭발적으로 증가하고 있죠. NestJS와 Angular는 이러한 엔터프라이즈급 SaaS 애플리케이션을 구축하기 위한 강력한 기술 스택입니다.

왜 이 조합인가?

  • TypeScript 공유: 프론트엔드와 백엔드가 동일한 언어를 사용하여 타입 안전성 보장
  • 모듈 아키텍처: 두 프레임워크 모두 모듈 기반 설계로 코드 구조화 용이
  • 강력한 DI 시스템: 의존성 주입을 통한 테스트 용이성과 유지보수성
  • 활발한 커뮤니티: 풍부한 라이브러리와 플러그인 생태계

Next.js와의 상세 비교가 궁금하시다면, NestJS + Angular vs Next.js 심층 비교 글을 참고하세요.

프로젝트 구조 설계

Nx 모노레포를 사용하여 프로젝트를 구성합니다:

saas-project/
├── apps/
│   ├── api/                # NestJS 백엔드
│   │   ├── src/
│   │   │   ├── auth/       # 인증 모듈
│   │   │   ├── billing/    # 결제 모듈
│   │   │   ├── tenants/    # 테넌트 관리
│   │   │   └── users/      # 사용자 관리
│   ├── web/                # Angular 프론트엔드
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── auth/
│   │   │   │   ├── dashboard/
│   │   │   │   └── shared/
├── libs/                   # 공유 라이브러리
│   ├── shared-types/       # 공유 타입 정의
│   └── shared-utils/       # 공유 유틸리티

이 구조를 통해 코드 재사용성을 극대화하고, 프론트엔드와 백엔드 간의 타입을 공유할 수 있습니다.

Cloudrix SaaS Starter ships with this pre-configured and tested.

Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.

Try the live demo →

인증 시스템 구현

SaaS 애플리케이션의 기반이 되는 JWT 인증 시스템을 구축합니다.

백엔드 인증 서비스

// auth.service.ts
@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService,
  ) {}

  async register(registerDto: RegisterDto) {
    const existingUser = await this.usersService.findByEmail(
      registerDto.email,
    );

    if (existingUser) {
      throw new ConflictException('이미 등록된 이메일입니다');
    }

    const hashedPassword = await bcrypt.hash(registerDto.password, 12);
    const user = await this.usersService.create({
      ...registerDto,
      password: hashedPassword,
    });

    // 새 테넌트 자동 생성
    const tenant = await this.tenantsService.create({
      name: registerDto.companyName,
      ownerId: user.id,
      plan: 'free',
    });

    return this.generateTokens(user, tenant.id);
  }

  async login(loginDto: LoginDto) {
    const user = await this.usersService.findByEmail(loginDto.email);

    if (!user || !(await bcrypt.compare(loginDto.password, user.password))) {
      throw new UnauthorizedException('이메일 또는 비밀번호가 올바르지 않습니다');
    }

    return this.generateTokens(user, user.tenantId);
  }

  private generateTokens(user: User, tenantId: string) {
    const payload = {
      sub: user.id,
      email: user.email,
      tenantId,
      role: user.role,
    };

    return {
      accessToken: this.jwtService.sign(payload, { expiresIn: '15m' }),
      refreshToken: this.jwtService.sign(payload, { expiresIn: '7d' }),
    };
  }
}

프론트엔드 인증 인터셉터

// auth.interceptor.ts
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getAccessToken();

  if (token) {
    req = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`,
      },
    });
  }

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        return authService.refreshToken().pipe(
          switchMap(newToken => {
            req = req.clone({
              setHeaders: {
                Authorization: `Bearer ${newToken.accessToken}`,
              },
            });
            return next(req);
          }),
          catchError(() => {
            authService.logout();
            return throwError(() => error);
          }),
        );
      }
      return throwError(() => error);
    }),
  );
};

멀티테넌트 데이터 격리

SaaS에서 가장 중요한 것 중 하나는 테넌트 간 데이터 격리입니다. TypeORM과 함께 Row-Level Security를 구현합니다:

// tenant.subscriber.ts
@EventSubscriber()
export class TenantSubscriber implements EntitySubscriberInterface {
  constructor(
    dataSource: DataSource,
    private readonly cls: ClsService,
  ) {
    dataSource.subscribers.push(this);
  }

  listenTo() {
    return BaseEntity; // 모든 엔티티에 적용
  }

  beforeInsert(event: InsertEvent<any>) {
    const tenantId = this.cls.get('tenantId');
    if (tenantId && event.entity) {
      event.entity.tenantId = tenantId;
    }
  }

  afterLoad(entity: any) {
    const tenantId = this.cls.get('tenantId');
    if (tenantId && entity.tenantId && entity.tenantId !== tenantId) {
      throw new ForbiddenException('접근 권한이 없습니다');
    }
  }
}

구독 결제 시스템

Stripe 결제 연동을 활용한 구독 결제 시스템을 구현합니다. 더 자세한 Stripe 구현 가이드는 Stripe Subscriptions 완전 가이드를 참고하세요.

이 모든 코드를 직접 작성할 필요가 없습니다. SaaS Starter에는 인증, 결제, 멀티테넌시가 55개 이상의 테스트와 함께 포함되어 있습니다. 무료 Lite 버전 시작하기 →

노트북에서 코딩하는 모습

// billing.controller.ts
@Controller('billing')
@UseGuards(JwtAuthGuard)
export class BillingController {
  constructor(private readonly billingService: BillingService) {}

  @Post('subscribe')
  async createSubscription(
    @CurrentTenant() tenantId: string,
    @Body() dto: CreateSubscriptionDto,
  ) {
    const session = await this.billingService.createCheckoutSession(
      tenantId,
      dto.planId,
    );
    return { checkoutUrl: session.url };
  }

  @Get('plans')
  async getPlans() {
    return [
      { id: 'starter', name: '스타터', price: 29000, currency: 'KRW' },
      { id: 'pro', name: '프로', price: 79000, currency: 'KRW' },
      { id: 'enterprise', name: '엔터프라이즈', price: 199000, currency: 'KRW' },
    ];
  }

  @Post('webhook')
  async handleWebhook(
    @Headers('stripe-signature') signature: string,
    @Req() req: RawBodyRequest<Request>,
  ) {
    return this.billingService.handleWebhook(req.rawBody, signature);
  }
}

Angular 대시보드 구현

Angular Signals를 활용한 반응형 대시보드를 구축합니다:

// dashboard.component.ts
@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [StatsCardComponent, ChartComponent, DatePipe],
  template: `
    <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
      @for (stat of stats(); track stat.label) {
        <app-stats-card
          [label]="stat.label"
          [value]="stat.value"
          [change]="stat.change"
          [icon]="stat.icon"
        />
      }
    </div>
    <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
      <app-chart
        title="월간 매출 추이"
        [data]="revenueData()"
        type="line"
      />
      <app-chart
        title="사용자 증가 추이"
        [data]="userData()"
        type="bar"
      />
    </div>
  `,
})
export class DashboardComponent {
  private readonly analyticsService = inject(AnalyticsService);

  stats = toSignal(this.analyticsService.getStats(), {
    initialValue: [],
  });

  revenueData = toSignal(this.analyticsService.getRevenueData());
  userData = toSignal(this.analyticsService.getUserGrowthData());
}

RBAC (역할 기반 접근 제어)

멀티테넌트 아키텍처와 RBAC의 상세 구현 패턴에 대해서는 NestJS 멀티테넌시 아키텍처 가이드를 참조하세요.

// roles.decorator.ts
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);

// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.includes(user.role);
  }
}

Docker를 활용한 배포

# docker-compose.prod.yml
services:
  api:
    build:
      context: .
      dockerfile: apps/api/Dockerfile
      target: production
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:pass@db:5432/saas
    depends_on:
      db:
        condition: service_healthy

  web:
    build:
      context: .
      dockerfile: apps/web/Dockerfile
    ports:
      - "80:80"
      - "443:443"

  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

직접 구축 vs 보일러플레이트 활용

SaaS 애플리케이션을 처음부터 구축하면 인증, 결제, 멀티테넌시, 관리자 대시보드 등 기본 기능만으로 4~6개월이 소요됩니다.

SaaS Starter는 이 모든 기능을 포함한 NestJS + Angular 보일러플레이트입니다:

  • 무료 Lite 버전으로 기본 기능 체험 가능
  • Pro ($149): 인증 + 결제 + 기본 대시보드
  • Business ($249): 멀티테넌시 + RBAC + 고급 기능
  • Enterprise ($399): 전체 기능 + 우선 지원

마무리: 지금 바로 시작하세요

NestJS와 Angular의 조합은 한국 시장에서 요구하는 높은 수준의 보안성, 확장성, 유지보수성을 모두 충족하는 기술 스택입니다. TypeScript의 타입 안전성과 모듈 기반 아키텍처를 활용하면 팀 규모에 관계없이 효율적으로 SaaS 제품을 개발할 수 있습니다.

대시보드 UI 화면

개발 속도를 높이고 싶다면, SaaS Starter 라이브 데모를 확인해 보세요. 무료 Lite 버전으로 시작하여 프로젝트 규모에 맞게 확장할 수 있습니다.

요금 안내:

  • Lite (무료) — 기본 아키텍처 체험
  • Pro ($149) — 인증 + 결제 + 대시보드
  • Business ($249) — 멀티테넌시 + RBAC + 고급 기능
  • Enterprise ($399) — 모든 기능 + 우선 지원

요금 상세 보기 | 라이브 데모 체험 | 무료 Lite 버전 다운로드

관련 글: Angular 관리자 대시보드 가이드 | NestJS REST API 튜토리얼 | AWS 배포 가이드

F

Firas Sayah

Senior Software Engineer

Full-stack developer with 5+ years building production SaaS applications with NestJS and Angular.