Next.js
Next.js 서버 액션을 안전하게 수정하는 법
서버 액션은 클라이언트와 서버 사이의 경계이기 때문에, 수정할 때 조심해야 할 부분들이 있다.
Next.js의 서버 액션은 강력하지만, 클라이언트와 서버의 경계에 있기 때문에 수정할 때 신경 써야 할 것들이 많다.
현재 서버 액션의 형태를 파악한다
먼저 기존 서버 액션이 뭘 하는지 정리한다. 문서화가 있으면 좋지만, 없다면 코드를 읽어야 한다.
'use server'
export async function createUser(formData: FormData) {
const name = formData.get('name')
const email = formData.get('email')
// DB에 저장
await db.users.create({ name, email })
}
호출하는 클라이언트 코드도 찾아본다. 서버 액션을 어디서 쓰고 있는지 알아야 안전하게 수정할 수 있다.
시그니처는 최대한 보존한다
함수의 인자나 반환값을 바꾸면 클라이언트에서 호출하는 쪽이 깨진다.
기존:
export async function createUser(formData: FormData) { }
수정하고 싶으면:
// 좋은 방법: 기존 함수 이름은 유지, 새로운 함수 추가
export async function createUser(formData: FormData) { }
export async function createUserWithValidation(data: UserInput) { }
꼭 기존 함수를 수정해야 한다면, 버전을 명시하는 것도 방법이다:
export async function createUserV2(data: UserInput) { }
한 번에 한 가지만 수정한다
서버 액션의 로직을 크게 수정해야 한다면, 여러 단계로 나눈다.
1단계: 기존 함수 유지하면서 새로운 내부 함수 만들기
async function createUserImpl(data: UserInput) {
// 새로운 로직
}
export async function createUser(formData: FormData) {
const data = parseFormData(formData)
return createUserImpl(data)
}
2단계: 클라이언트에서 새로운 함수로 천천히 전환 3단계: 기존 함수 제거
에러 처리를 확인한다
서버 액션에서 에러가 나면 클라이언트에서도 제대로 받아야 한다.
'use server'
export async function createUser(data: UserInput) {
try {
await db.users.create(data)
return { success: true }
} catch (error) {
// 클라이언트에서 받을 수 있는 형태로
return { success: false, error: error.message }
}
}
클라이언트에서도 이 응답을 처리해야 한다:
const result = await createUser(data)
if (!result.success) {
setError(result.error)
}
타입을 명시한다
TypeScript를 쓴다면 서버 액션의 입력과 출력 타입을 명확히 정의한다.
interface UserInput {
name: string
email: string
}
interface CreateUserResult {
success: boolean
userId?: string
error?: string
}
export async function createUser(data: UserInput): Promise<CreateUserResult> {
// ...
}
보안 관점에서 검증한다
서버 액션은 클라이언트에서 호출되지만, 서버에서 실행된다. 따라서 모든 입력을 검증해야 한다.
export async function createUser(data: UserInput) {
// 1. 타입 검증
if (!data.name || !data.email) {
throw new Error('Invalid input')
}
// 2. 권한 검증
const session = await auth()
if (!session) {
throw new Error('Unauthorized')
}
// 3. 비즈니스 로직 검증
const existing = await db.users.findUnique({ where: { email: data.email } })
if (existing) {
throw new Error('Email already exists')
}
}
데이터베이스 마이그레이션과 함께 한다
서버 액션이 새로운 DB 스키마를 기대한다면, 배포 순서가 중요하다.
- 데이터베이스 마이그레이션 배포
- 기존 서버 액션과 새로운 서버 액션이 공존
- 클라이언트 코드 업데이트
- 기존 서버 액션 제거
결론
서버 액션을 수정할 때는 시그니처를 보존하고, 한 번에 한 가지만 바꾸고, 모든 입력을 검증해야 한다. 점진적인 마이그레이션이 가장 안전한 방법이다.