공통스키마와 코어로직, UI로직 분리를 통한 역할과 책임 구분
프론트엔드 개발에서 스키마, 코어 로직, UI 로직을 분리하는 것은 애플리케이션의 각 부분을 명확하게 구분하고, 각자의 역할을 독립적으로 수행하도록 설계하는 중요한 전략이다. 이러한 분리는 유지보수성과 확장성을 높이는 데 큰 도움이 된다. 이번 글에서는 스키마, 코어 로직, UI 로직의 역할과 분리의 필요성에 대해 적어보려 한다.
1. 스키마 (Schema)
스키마는 데이터 구조와 유효성 검사를 정의한다. 주로 입력 데이터의 형태를 명확히 하고, 데이터가 올바른지 검증하는 기능을 담당한다.
데이터의 유효성 검사를 별도로 관리하면 UI나 비즈니스 로직이 변경되더라도 스키마는 그대로 유지된다. 또한, 동일한 스키마를 여러 곳에서 재사용할 수 있어 코드의 중복을 줄이고, 유효성 검사의 일관성을 유지할 수 있다.
예시
// src/schemas/userSchema.ts
import { z } from 'zod';
export const userSchema = z.object({
name: z.string().min(2, "이름은 최소 2자 이상이어야 합니다."),
email: z.string().email("유효한 이메일 주소를 입력하세요."),
age: z.number().min(18, "18세 이상이어야 합니다."),
});
export type UserType = z.infer<typeof userSchema>;
Zod 소개 및 장점
예시에서 활용했던, 스키마를 작성하는 데 있어 효율적인 라이브러리로 Zod를 소개/추천한다. Zod는 TypeScript와 완벽하게 호환되는 선언적 스키마 정의 라이브러리로, 여러가지 장점이 있다.
1. TypeScript와의 완벽한 통합: Zod는 TypeScript와의 호환성이 뛰어나며, 스키마에서 자동으로 타입을 유추할 수 있다. 이를 통해 타입 안전성을 보장하고, 개발자가 타입을 별도로 정의할 필요가 없어진다.
2. 간결하고 선언적인 문법: Zod는 간결하고 읽기 쉬운 문법을 제공하여 스키마 정의를 쉽게 할 수 있다. 이는 코드의 가독성을 높이고, 유지보수를 용이하게 만든다.
3. 강력한 유효성 검사 기능: Zod는 다양한 내장 유효성 검사 메서드를 제공하며, 커스텀 유효성 검사를 쉽게 추가할 수 있다. 이를 통해 복잡한 데이터 구조와 요구사항을 효율적으로 처리할 수 있다.
const extendedUserSchema = userSchema.extend({
password: z.string().min(8, "비밀번호는 최소 8자 이상이어야 합니다."),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "비밀번호가 일치하지 않습니다.",
path: ["confirmPassword"],
});
4. 런타임 검증과 타입 안전성: Zod는 런타임 시점에 데이터를 검증할 수 있으며, 타입스크립트 타입과 동기화되어 타입 안전성을 유지한다. 이는 버그를 사전에 방지하고, 안정적인 애플리케이션을 개발하는 데 기여한다.
5. 유연한 확장성: Zod는 스키마를 쉽게 확장하고 조합할 수 있는 기능을 제공한다. 이를 통해 복잡한 데이터 구조를 효율적으로 관리할 수 있다.
2. 코어 로직 (Core Logic)
코어 로직은 애플리케이션의 비즈니스 로직을 처리한다. 데이터 처리, API 호출, 데이터베이스 작업 등 중요한 로직을 수행하며, 애플리케이션의 핵심 기능을 담당한다.
코어 로직을 UI와 분리하면 비즈니스 로직이 UI의 변경에 영향을 받지 않고 독립적으로 동작할 수 있다. 또한, 비즈니스 로직만 별도로 테스트할 수 있어 테스트가 용이해진다.
예시
// src/services/userService.ts
import { userSchema, UserType } from '../schemas/userSchema';
export const createUser = (data: unknown): UserType => {
// 스키마를 사용해 데이터 유효성 검사
const parsedData = userSchema.parse(data);
// 비즈니스 로직 수행
// 예: 데이터베이스에 사용자 저장
// 여기서는 단순히 데이터를 반환하지만 실제로는 DB 저장 로직이 들어감
return parsedData;
};
이 코어 로직에서는 userSchema를 사용해 데이터를 검증하고, 검증된 데이터를 바탕으로 사용자 생성 로직을 수행한다. 코어 로직은 UI와는 독립적으로 동작한다.
3. UI 로직 (UI Logic)
UI 로직은 사용자 인터페이스와의 상호작용을 처리한다. 폼 상태 관리, 유효성 검사 메시지 표시, 사용자 입력 처리 등이 포함된다. 사용자 경험을 향상시키는 데 중점을 둔다.
UI 로직을 별도로 분리하면 사용자 인터페이스에 집중할 수 있다. 비즈니스 로직이 UI에 직접 포함되면, UI 변경 시 비즈니스 로직도 영향을 받아 유지보수가 어려워진다. 반대로, UI 로직이 분리되면 UI의 변화가 비즈니스 로직에 영향을 주지 않는다.
예시
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { userSchema, UserType } from '../schemas/userSchema';
import { createUser } from '../services/userService';
const UserForm: React.FC = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<UserType>({
resolver: zodResolver(userSchema),
});
const [result, setResult] = useState<UserType | null>(null);
const onSubmit = (data: UserType) => {
const userData = createUser(data);
setResult(userData);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>이름:</label>
<input {...register('name')} />
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label>이메일:</label>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<label>나이:</label>
<input {...register('age')} type="number" />
{errors.age && <p>{errors.age.message}</p>}
</div>
<button type="submit">제출</button>
{result && <pre>{JSON.stringify(result, null, 2)}</pre>}
</form>
);
};
export default UserForm;
이 컴포넌트는 사용자 입력을 처리하고, 폼 상태를 관리하며, 제출된 데이터를 createUser 코어 로직에 전달한다. zodResolver를 통해 스키마로 유효성 검사를 관리한다. 코어 로직과 UI 로직이 명확히 분리되어 있어 각 부분을 독립적으로 유지보수할 수 있다.
4. 전체적인 구조
스키마, 코어 로직, UI 로직을 분리하면 코드의 가독성과 유지보수성이 크게 향상된다. 예를 들어, UI 로직과 비즈니스 로직을 독립적으로 테스트할 수 있고, 각 로직이 변경되더라도 다른 부분에 미치는 영향을 최소화할 수 있다.
파일 구조
// src/
├── components/
│ └── UserForm.tsx // UI 로직
├── schemas/
│ └── userSchema.ts // 스키마 정의
├── services/
│ └── userService.ts // 코어 로직
이 구조에서는 각 부분이 서로 독립적으로 작동한다. UI 로직은 비즈니스 로직에 의존하지만, 반대로 비즈니스 로직은 UI 로직에 의존하지 않는다. 이는 유지보수 및 확장 시 큰 장점을 제공한다.
유지보수 및 확장성 향상
1. 독립적인 테스트: 각 부분을 독립적으로 테스트할 수 있다. 예를 들어, 비즈니스 로직이 올바르게 작동하는지, UI가 올바르게 사용자 입력을 처리하는지 별도로 테스트할 수 있다.
2. 코드 재사용성: 스키마는 여러 컴포넌트에서 재사용될 수 있다. 동일한 데이터 구조를 사용하는 다른 폼이 있을 경우, 동일한 스키마를 사용해 유효성 검사를 재사용할 수 있다.
3. 변경 용이성: UI 로직과 코어 로직이 분리되어 있기 때문에, UI의 디자인이 변경되더라도 비즈니스 로직은 영향을 받지 않는다. 반대로, 비즈니스 로직이 변경되더라도 UI 로직을 그대로 유지할 수 있다.
5. 개인적 경험 및 생각
프로젝트를 진행하면서 스키마, 코어 로직, UI 로직을 철저히 분리했을 때의 장점을 실감했다. 초기에는 파일 구조가 복잡해 보였지만, 시간이 지나면서 각 부분의 역할이 명확해져 유지보수가 훨씬 수월해졌다. 특히, 팀원들과의 협업 시 역할 분담이 명확해져 개발 속도가 빨라졌고, 버그 발생 시 원인을 쉽게 파악할 수 있다.
또한, 스키마를 중심으로 데이터 유효성을 관리하면서 데이터 일관성을 유지할 수 있었다. 예를 들어, 사용자 정보가 여러 컴포넌트에서 사용될 때, 동일한 스키마를 참조함으로써 중복 코드를 줄이고, 일관된 검증 로직을 적용할 수 있었다. 이는 코드의 재사용성을 높이고, 버그를 줄이는 데 도움이 되었다.
확장성을 높이는 데도 기여했다. 새로운 기능을 추가할 때 기존 로직에 미치는 영향을 최소화할 수 있었고, 필요한 부분만을 수정하거나 확장할 수 있었다. 이는 장기적으로 개발의 안정성과 지속 가능성을 높이는 데 중요한 요소다.