저는 플러터 개발자로 2년 6개월 정도 실무를 해오면서 프런트엔드와 모바일 개발에 집중해 왔습니다. 사이드 프로젝트에서는 자연스럽게 백엔드까지 다루게 되었고, 그 과정에서 NestJS를 사용해 토이 프로젝트 백엔드를 직접 구현한 경험이 있습니다. 풀스택 개발 자체를 좋아했지만, 언어가 나눠져 있다 보니 생산성이 떨어지거나 API 스펙을 두 번 관리해야 하는 번거로움도 있었습니다.
그런 가운데 우연히 Dart 풀스택을 사용하는 회사에 합류하면서 Serverpod를 본격적으로 경험하게 되었습니다. Flutter와 동일한 언어인 Dart로 백엔드까지 개발할 수 있다는 점이 흥미로웠고, 실제로 NestJS를 사용할 때보다 개발 속도가 확연히 빨라졌습니다. 타입 체킹도 강력하고, RPC 기반 자동 코드 생성 덕분에 프런트와 서버 간 스펙 불일치 문제도 자연스럽게 해결되었습니다.
물론 Serverpod는 아직 모든 부분이 100% 안정적인 프레임워크는 아닙니다. 하지만 개인 프로젝트와 실무에서 약 6개월간 사용해본 결과, 스타트업이나 사이드 프로젝트에서는 충분히 실용적이고, 생산성을 크게 올릴 수 있는 도구라고 판단했습니다.
이 글에서는 제가 지난 6개월 동안 Serverpod를 사용하며 느낀 점을 정리하고, 익숙한 NestJS 코드와 비교해가며 간단한 메모 기능 예제를 만들어 보겠습니다.
Flutter로 프런트, NestJS로 백엔드를 개발하던 시절에는 “풀스택”이라고 해도 실질적으로는 두 언어, 두 개발 환경, 두 타입 시스템을 동시에 관리해야 했습니다.
Serverpod를 사용하면서 느낀 가장 큰 변화는 바로 “하나의 언어(Dart)로 모든 계층을 다룰 수 있다”, 그리고 “백엔드 개발 흐름이 Flutter 개발 방식과 훨씬 닮아 있다”는 점이었습니다.
특히 NestJS처럼 일반적인 백엔드 프레임워크와 비교했을 때 생산성이 크게 올라갔습니다.
아래에서 직접 사용 경험을 기준으로 그 차이를 정리해보겠습니다.
| 항목 | NestJS | Serverpod |
|---|---|---|
| 언어 | TS | Dart |
| 모델 동기화 | 수동 | 자동 생성 |
| 인증 | JWT 직접 구현 | Session 자동 |
| DB 마이그레이션 | TypeORM 셋업 필요 | 자동 생성 및 스키마 추적 |
| 인프라 | 직접 설정 | 프로젝트 생성 시 포함 |
| 개발 속도 | 느림 | 빠름 |
토이 프로젝트에서 NestJS를 사용했을 때, 몇 가지 불편한 점이 있었습니다.
1. 타입 안정성의 부재
NestJS에서 TypeScript를 사용하더라도, 클라이언트와 서버 간의 타입이 완전히 동기화되지 않았습니다. API 스펙을 변경하면 클라이언트 코드도 수동으로 수정해야 했고, 실수로 타입이 맞지 않는 경우 런타임 에러가 발생했습니다.
// NestJS: 서버에서 User 인터페이스 정의
interface User {
id: number;
name: string;
email: string;
}
// 클라이언트에서도 동일하게 정의해야 함 (수동)
interface User {
id: number;
name: string;
email: string;
}
2. 인증 관리의 복잡함
JWT 토큰을 수동으로 관리해야 했습니다. Access Token, Refresh Token을 저장하고, 만료 시 갱신하는 로직을 직접 구현해야 했습니다.
// NestJS: JWT 토큰 수동 관리
const accessToken = await this.jwtService.sign(payload);
const refreshToken = await this.jwtService.sign(payload, { expiresIn: '7d' });
// 클라이언트에서도 토큰 저장 및 갱신 로직 필요
await AsyncStorage.setItem('accessToken', accessToken);
await AsyncStorage.setItem('refreshToken', refreshToken);
개발자 입장에서 반복적인 코드가 너무 많았고, 에러가 날 여지가 많았습니다.
3. 개발 속도의 한계
백엔드와 프론트엔드를 분리해서 개발하다 보니, API 스펙을 정의하고, 서버를 구현하고, 클라이언트를 구현하는 과정이 반복되었습니다. 작은 변경사항도 여러 곳을 수정해야 했습니다.
Serverpod를 사용하면서 위의 문제들이 해결되었습니다.
1. 완벽한 타입 안정성
Serverpod는 서버에서 모델을 정의하면
클라이언트 Dart 코드가 자동으로 생성됩니다.
타입 불일치가 사실상 존재하지 않고,
API·모델 스펙을 “하나의 소스”에서 관리할 수 있습니다.
2. 자동 인증 관리
Serverpod는 기본적으로 Session 기반 인증을 제공합니다.
로그인 성공 시 세션 생성
클라이언트는 별도 토큰 관리 필요 없음
모든 요청에 세션이 자동 포함됨
JWT처럼 Access/Refresh Token 저장·갱신을 신경 쓸 필요가 없어서,
전체 인증 플로우가 훨씬 단순해졌습니다.
3. 빠른 개발 속도
Flutter 개발자의 입장에서 가장 크게 느끼는 장점은 이것입니다.
“언어도 같고, 리포지토리 구조도 비슷하고, 비동기 처리나 개발 패턴도 거의 동일하다.”
반복되는 작업을 Serverpod가 자동화해주기 때문에 NestJS를 사용할 때보다 개발 속도가 훨씬 빨라졌습니다.
# Serverpod CLI 설치
dart pub global activate serverpod_cli
# 프로젝트 생성
serverpod create test
프로젝트가 생성되면 다음과 같은 구조가 만들어집니다:
test/
├── test_client/ # 생성된 클라이언트 코드
├── test_server/ # 서버 코드
└── test_flutter/ # Flutter 앱
서버 코드를 살펴보면 Dockerfile, docker-compose.yaml, Terraform 설정 파일들까지 자동으로 생성되어 있어 인프라 관리가 훨씬 수월합니다.
Serverpod는 단순히 API 서버 역할을 넘어, 서버 배포와 운영에 필요한 기본적인 DevOps 구성 요소를 프로젝트 템플릿 단계에서 제공하기 때문에 초기에 인프라를 구축하는 시간이 크게 줄어듭니다.
특히 AWS 관련 디렉토리에는 다음과 같은 리소스 코드가 포함되어 있습니다:
로드밸런서, VPC, RDS, Redis, S3 스토리지, CloudFront 배포 등
실제 서버 운영에 필요한 인프라를 코드 기반으로 생성할 수 있도록 제공

(다음 글에서는 테라폼을 사용하여 s3 버킷을 구축하고 실제로 flutter + serverpod를 사용하여 PresignedUrl 방식으로 파일을 업로드하는 과정을 정리해볼 예정입니다.)
NestJS에서는 데이터베이스를 직접 설치하고 설정해야 했지만, Serverpod는 Docker Compose로 모든 인프라를 자동으로 설정합니다.
cd test_server
# Docker Compose로 PostgreSQL, Redis 등 자동 실행
docker compose up --build --detach
# 서버 실행
dart run bin/main.dart

NestJS와 비교:
# NestJS: 수동으로 PostgreSQL 설치 및 설정 필요
# 1. PostgreSQL 설치
# 2. 데이터베이스 생성
# 3. TypeORM 설정
# 4. 마이그레이션 실행
npm run migration:run
# Serverpod: Docker Compose로 한 번에 해결
docker compose up --build --detach
Serverpod 방식:
# test_server/lib/src/protocol/memo.spy.yaml
class: Memo
table: memo
fields:
title: String
content: String
createdAt: DateTime?
updatedAt: DateTime?
# 코드 생성 (모델, 클라이언트 코드 자동 생성)
serverpod generate

백엔드 코드 수정 (모델 추가 , 엔드포인트 추가 등등)시에는 해당 명령어를 사용해 클라이언트 코드를 재생성해줘야 flutter에서 해당 모델 또는 엔드포인트를 호출 할수있습니다.
NestJS 방식:
// Entity 정의
@Entity()
export class Memo {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
content: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// DTO 정의
export class CreateMemoDto {
title: string;
content: string;
}
// 클라이언트에서도 별도로 타입 정의 필요
Serverpod는 *.spy.yaml 파일 하나로 서버 모델과 클라이언트 타입이 모두 생성됩니다.
Serverpod 방식:
# 마이그레이션 생성 (모델 변경 후)
serverpod create-migration
# 마이그레이션 실행
dart run bin/main.dart --apply-migrations

NestJS 방식:
# 마이그레이션 생성
npm run migration:generate -- -n CreateMemo
# 마이그레이션 실행
npm run migration:run
Serverpod는 모델 변경 사항을 자동으로 감지하여 마이그레이션을 생성합니다.
참고: 이 글은 Serverpod 사용 방식에 대한 포스팅입니다. 아래 Flutter 코드는 Serverpod의 사용법을 설명하기 위한 예시 코드이며, 아키텍처나 Flutter에 대한 상세한 설명은 포함되어 있지 않습니다. 실제 프로덕션 환경에서는 프로젝트의 요구사항에 맞게 아키텍처를 설계하시기 바랍니다.
Serverpod 방식:
// test_server/lib/src/endpoint/memo_endpoint.dart
import 'package:serverpod/serverpod.dart';
import 'package:test_server/src/generated/protocol.dart';
class MemoEndpoint extends Endpoint {
// 메모 목록 조회
Future<List<Memo>> getMemos(Session session) async {
return await Memo.db.find(session);
}
// 메모 생성
Future<Memo> createMemo(Session session, Memo memo) async {
return await Memo.db.insertRow(session, memo);
}
// 메모 수정
Future<Memo> updateMemo(Session session, Memo memo) async {
return await Memo.db.updateRow(session, memo);
}
// 메모 삭제
Future<bool> deleteMemo(Session session, int id) async {
return await Memo.db.deleteRow(session, id);
}
}
NestJS 방식:
// Controller
@Controller('memos')
export class MemoController {
constructor(private readonly memoService: MemoService) {}
@Get()
async getMemos(): Promise<Memo[]> {
return this.memoService.findAll();
}
@Post()
async createMemo(@Body() createMemoDto: CreateMemoDto): Promise<Memo> {
return this.memoService.create(createMemoDto);
}
}
// Service
@Injectable()
export class MemoService {
constructor(
@InjectRepository(Memo)
private memoRepository: Repository<Memo>,
) {}
async findAll(): Promise<Memo[]> {
return this.memoRepository.find();
}
async create(createMemoDto: CreateMemoDto): Promise<Memo> {
const memo = this.memoRepository.create(createMemoDto);
return this.memoRepository.save(memo);
}
}
Serverpod는 Controller와 Service를 분리할 필요 없이 Endpoint 하나로 처리할 수 있습니다.
클린 아키텍처를 적용하여 백엔드 변경에 유연하게 대응할 수 있도록 구성했습니다.
중요: Serverpod 모델 사용 방식
Serverpod에서는 Flutter 클라이언트에서 별도의 Entity 클래스를 생성하지 않고, serverpod generate 명령어로 자동 생성된 pod.Memo 형태로 직접 접근하는 것을 권장합니다.
// ✅ 권장: pod.Memo 사용 (자동 생성된 코드)
import 'package:test_client/test_client.dart' as pod;
final memo = pod.Memo(
title: '제목',
content: '내용',
);
// ❌ 비권장: 별도 Entity 클래스 생성
class MemoEntity {
final int? id;
final String title;
// ...
}
이렇게 하면:
serverpod generate만 실행하면 클라이언트 코드가 자동으로 업데이트됩니다
lib/
├── core/
│ ├── domain/
│ │ ├── failure/
│ │ └── usecase/
│ └── di/
├── feature/
│ └── memo/
│ ├── data/
│ │ ├── datasource/
│ │ └── repository/
│ ├── domain/
│ │ ├── entity/
│ │ ├── repository/
│ │ └── usecase/
│ └── presentation/
│ ├── provider/
│ └── screen/
#### Repository 구현
**Serverpod 방식:**
```dart
//lib/feature/memo/data/repository/memo_repository_impl.dart
import 'package:test_client/test_client.dart' as pod;
import '../../domain/repository/memo_repository.dart';
class MemoRepositoryImpl implements MemoRepository {
final Client client;
MemoRepositoryImpl(this.client);
@override
Future<List<pod.Memo>> getMemos() async {
// 타입 안전한 API 호출 (자동 생성된 코드)
return await client.memo.getMemos();
}
@override
Future<pod.Memo> createMemo(pod.Memo memo) async {
return await client.memo.createMemo(memo);
}
}
NestJS 방식 (Dio 사용):
// Repository
class MemoRepositoryImpl implements MemoRepository {
final Dio dio;
MemoRepositoryImpl(this.dio);
Future<List<Memo>> getMemos() async {
// 수동으로 타입 변환 필요
final response = await dio.get('/memos');
return (response.data as List)
.map((json) => Memo.fromJson(json))
.toList();
}
}
Serverpod는 타입 안전한 API 호출이 자동으로 생성되어, 수동으로 JSON 변환할 필요가 없습니다.
// lib/feature/memo/domain/usecase/get_memos_usecase.dart
import 'package:dartz/dartz.dart';
import '../../../../core/domain/usecase/usecase.dart';
import '../domain.dart';
class GetMemosUseCase
implements UseCase<List<pod.Memo>, void, MemoRepository> {
final MemoRepository repository;
const GetMemosUseCase(this.repository);
MemoRepository get repo => repository;
Future<Either<Failure, List<pod.Memo>>> call(void param) async {
try {
final result = await repository.getMemos();
return Right(result);
} on Exception catch (e) {
return Left(
GetMemosFailure(
'메모 목록을 불러올 수 없습니다.',
exception: e,
),
);
}
}
}
// lib/feature/memo/presentation/provider/memo_notifier.dart
class MemoNotifier extends StateNotifier<MemoState> {
final GetMemosUseCase getMemosUseCase;
MemoNotifier(this.getMemosUseCase) : super(const MemoState());
Future<void> loadMemos() async {
state = state.copyWith(isLoading: true);
final result = await getMemosUseCase(null);
result.fold(
(failure) {
state = state.copyWith(
isLoading: false,
error: failure.message,
);
},
(memos) {
state = state.copyWith(
memos: memos,
isLoading: false,
);
},
);
}
}


자세한 소스코드는 해당 링크에서 확인 부탁드립니다 !
https://github.com/pyowonsik/FlutterServerpod/tree/main/test_flutter
# 1. 프로젝트 생성
serverpod create test
# 2. Docker Compose로 인프라 실행
cd test_server
docker compose up --build --detach
# 3. 모델 정의 후 코드 생성
serverpod generate
# 4. 마이그레이션 생성 (모델 변경 시)
serverpod create-migration
# 5. 마이그레이션 실행
dart run bin/main.dart --apply-migrations
# 6. 서버 실행
dart run bin/main.dart
앞으로 예제 코드로 작성될 모든 코드의 아키텍처는 이런식으로 클린아키텍처를 사용할 예정입니다. 백엔드가 Serverpod에서 Spring, Node.js, Python으로 바뀌어도 Data 레이어만 변경하면 됩니다.
Domain Layer (변경 없음)
↑
Repository Interface (변경 없음)
↑
Data Layer (Serverpod → Spring 변경 시 여기만 수정)
이렇게 하면:
1. 빠른 개발 속도
NestJS에서는 API 스펙을 정의하고, 서버를 구현하고, 클라이언트를 구현하는 과정이 반복되었습니다. Serverpod는 모델 정의만으로 서버와 클라이언트 코드가 자동 생성되어 개발 속도가 크게 향상되었습니다.
2. 타입 안정성
NestJS에서는 클라이언트와 서버 간 타입이 동기화되지 않아 런타임 에러가 발생할 수 있었습니다. Serverpod는 자동 생성된 코드로 타입 안정성이 보장됩니다.
3. 인증 관리 간편
NestJS에서는 JWT 토큰을 수동으로 관리해야 했지만, Serverpod는 Session 기반 인증으로 자동 관리됩니다.
4. 인프라 설정 자동화
NestJS에서는 데이터베이스, Redis 등을 수동으로 설치하고 설정해야 했지만, Serverpod는 Docker Compose로 자동 설정됩니다.
1. 생태계가 작음
NestJS에 비해 자료가 적고 커뮤니티가 작습니다. 문제가 발생했을 때 해결 방법을 찾기 어려울 수 있습니다.
2. 아직 불안정한 부분
버전 업데이트 시 Breaking Change가 발생할 수 있고, 일부 기능이 미완성 상태입니다.
3. 학습 자료 부족
공식 문서 외 자료가 적고, 실무 경험 공유가 적습니다.
NestJS의 숙련도 부족, TypeScript의 이해도 부족 등으로 인해 백엔드 개발에 부담이 있었는데, Serverpod를 사용하면서 Dart만으로 빠르게 개발할 수 있어 부담이 줄었습니다.
개인 프로젝트나 스타트업 초기 단계에서는 충분히 사용 가능한 프레임워크라고 판단합니다.
https://github.com/pyowonsik/FlutterServerpod/tree/main/test_flutter
Serverpod는 Dart 풀스택 개발을 빠르게 할 수 있는 프레임워크입니다. NestJS에 비해 타입 안정성과 개발 속도 면에서 장점이 있지만, 생태계가 작고 아직 불안정한 부분이 있습니다.
다음 글에서는:
을 다루겠습니다.
참고 자료:
오 ~ serverpod 이란것도 있었군요~ 잘 보고 갑니다 !