Service 사용 개요
Namespace 호출
UserService.getUser()간결한 문법
타입 안전
자동 완성컴파일 검증
Subset 지원
필요한 필드만성능 최적화
TanStack Query
useUser Hook자동 캐싱
기본 사용법
Service Import
생성된 Service는services.generated.ts에서 Namespace로 export됩니다.
복사
// ✅ 올바른 import 방식
import { UserService, PostService } from "@/services/services.generated";
- 일관성: 모든 Service가 한 곳에서 관리됨
- 간편한 import: 파일 경로 찾을 필요 없음
- 자동 업데이트:
pnpm generate시 자동으로 동기화 - 명확한 네이밍: Namespace로 그룹화되어 충돌 없음
기본 API 호출
Service의 정적 함수를 직접 호출합니다.복사
import { UserService } from "@/services/services.generated";
// 사용자 조회 (Subset "A"로 기본 정보만)
const user = await UserService.getUser("A", 123);
console.log(user.username); // 타입 안전!
// 사용자 수정
await UserService.updateProfile({
username: "newname",
bio: "Hello, World!",
});
await으로 비동기 호출- 응답은 자동으로 파싱됨 (fetch 함수 내부 처리)
- 타입이 완전히 보장됨
- Subset 파라미터 필수 (getUser 등)
Namespace 기반 구조:
- ❌ 클래스 인스턴스:
new UserService()불필요 - ✅ 정적 함수:
UserService.getUser()직접 호출 - 모든 함수가 독립적으로 동작
Subset 시스템 이해하기
Sonamu의 핵심 기능인 Subset 시스템입니다.복사
// Subset "A": 기본 정보
const basicUser = await UserService.getUser("A", 123);
console.log(basicUser.id); // ✅ OK
console.log(basicUser.username); // ✅ OK
console.log(basicUser.bio); // ❌ 컴파일 에러 (Subset A에 없음)
// Subset "B": bio 포함
const userWithBio = await UserService.getUser("B", 123);
console.log(userWithBio.bio); // ✅ OK
// Subset "C": 전체 필드
const fullUser = await UserService.getUser("C", 123);
console.log(fullUser.createdAt); // ✅ OK
- 성능: 필요한 필드만 조회하여 네트워크 비용 절감
- 타입 안전: 각 Subset마다 정확한 타입 반환
- 명시성: 어떤 데이터가 필요한지 코드에서 명확히 표현
React에서 사용하기
TanStack Query Hook (권장)
가장 간편한 방법은 자동 생성된 TanStack Query Hook을 사용하는 것입니다.복사
import { UserService } from "@/services/services.generated";
function UserProfile({ userId }: { userId: number }) {
// 자동 생성된 Hook 사용
const { data: user, error, isLoading } = UserService.useUser("A", userId);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.username}</h1>
<p>{user.email}</p>
</div>
);
}
- 자동 캐싱 (같은 userId 재사용)
- 자동 재검증 (포커스 시, 재연결 시)
- 로딩/에러 상태 자동 관리
- 중복 요청 자동 제거
함수 컴포넌트 (useEffect)
Hook을 사용하지 않고 직접 호출할 수도 있습니다.복사
import { useState, useEffect } from "react";
import { UserService } from "@/services/services.generated";
import type { User } from "@/services/services.generated";
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
// Subset "C"로 전체 필드 조회
const userData = await UserService.getUser("C", userId);
setUser(userData);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load user");
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.username}</h1>
<p>{user.email}</p>
</div>
);
}
권장: 가능하면 TanStack Query Hook을 사용하세요. 자동 캐싱, 재검증, 상태 관리를 모두 제공하여 코드가 훨씬 간결해집니다.
이벤트 핸들러
사용자 액션에 따라 API를 호출합니다.복사
import { useState } from "react";
import { UserService } from "@/services/services.generated";
function EditProfile({ userId }: { userId: number }) {
const [username, setUsername] = useState("");
const [bio, setBio] = useState("");
const [saving, setSaving] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
try {
setSaving(true);
await UserService.updateProfile({
username,
bio,
});
alert("Profile updated!");
} catch (error) {
alert("Failed to update profile");
console.error(error);
} finally {
setSaving(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
/>
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
placeholder="Bio"
/>
<button type="submit" disabled={saving}>
{saving ? "Saving..." : "Save"}
</button>
</form>
);
}
Next.js에서 사용하기
Server Components (App Router)
Next.js 13+ App Router의 Server Components에서는 Service를 직접 호출할 수 없습니다. 대신 백엔드 모델을 직접 사용합니다.복사
// app/users/[id]/page.tsx
// ❌ Server Component에서는 작동하지 않음
// Service는 브라우저용 fetch를 사용하므로 서버에서 사용 불가
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await UserService.getUser("A", parseInt(params.id)); // ❌ 작동 안함!
return <div>{user.username}</div>;
}
복사
// app/users/[id]/page.tsx
import { UserModel } from "@/models/user.model";
export default async function UserPage({ params }: { params: { id: string } }) {
// 서버에서 모델을 직접 사용
const userModel = new UserModel();
const { user } = await userModel.getProfile(parseInt(params.id));
return (
<div>
<h1>{user.username}</h1>
<p>{user.email}</p>
</div>
);
}
권장 방식: Server Components에서는 백엔드 모델을 직접 사용하세요. HTTP 요청 오버헤드가 없고, 타입 안전성도 동일하게 보장됩니다.
Client Components
Client Components에서는 Service를 자유롭게 사용할 수 있습니다.복사
// app/users/[id]/edit-button.tsx
"use client";
import { useState } from "react";
import { UserService } from "@/services/services.generated";
export function EditButton({ userId }: { userId: number }) {
const [editing, setEditing] = useState(false);
async function handleEdit() {
setEditing(true);
try {
await UserService.updateProfile({
username: "newname",
});
alert("Updated!");
} catch (error) {
alert("Failed!");
} finally {
setEditing(false);
}
}
return (
<button onClick={handleEdit} disabled={editing}>
{editing ? "Saving..." : "Edit"}
</button>
);
}
에러 처리
SonamuError 처리
Service 호출 시 발생하는 에러를 타입 안전하게 처리합니다.복사
import { UserService } from "@/services/services.generated";
import { isSonamuError } from "@/lib/sonamu.shared";
async function updateUser() {
try {
await UserService.updateProfile({
username: "newname",
});
} catch (error) {
if (isSonamuError(error)) {
// Sonamu 에러 (타입 안전)
console.log("Status:", error.code);
console.log("Message:", error.message);
// Zod 유효성 검사 에러
error.issues.forEach((issue) => {
console.log(`${issue.path.join(".")}: ${issue.message}`);
});
// HTTP 상태 코드별 처리
if (error.code === 401) {
// 인증 에러
console.log("Please login");
} else if (error.code === 403) {
// 권한 에러
console.log("Permission denied");
} else if (error.code === 422) {
// Validation 에러
console.log("Invalid data:", error.issues);
}
} else {
// 일반 에러
console.log("Network error:", error);
}
}
}
React에서 에러 처리
복사
import { isSonamuError } from "@/lib/sonamu.shared";
function EditProfile({ userId }: { userId: number }) {
const [error, setError] = useState<string | null>(null);
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
async function handleSubmit(data: any) {
setError(null);
setValidationErrors({});
try {
await UserService.updateProfile(data);
} catch (err) {
if (isSonamuError(err)) {
setError(err.message);
// Zod 유효성 검사 에러를 필드별로 매핑
const fieldErrors: Record<string, string> = {};
err.issues.forEach((issue) => {
const field = issue.path.join(".");
fieldErrors[field] = issue.message;
});
setValidationErrors(fieldErrors);
} else {
setError("An unexpected error occurred");
}
}
}
return (
<div>
{error && <div className="error-message">{error}</div>}
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit({/* data */});
}}>
<div>
<input name="username" />
{validationErrors.username && (
<span className="error">{validationErrors.username}</span>
)}
</div>
</form>
</div>
);
}
고급 패턴
병렬 요청
여러 API를 동시에 호출하여 성능을 향상시킵니다.복사
import { UserService, PostService } from "@/services/services.generated";
async function loadUserDashboard(userId: number) {
// ❌ 순차 실행 (느림)
const user = await UserService.getUser("A", userId);
const posts = await PostService.getPostsByUser(userId);
const comments = await PostService.getCommentsByUser(userId);
return { user, posts, comments };
}
async function loadUserDashboardFast(userId: number) {
// ✅ 병렬 실행 (빠름)
const [user, posts, comments] = await Promise.all([
UserService.getUser("A", userId),
PostService.getPostsByUser(userId),
PostService.getCommentsByUser(userId),
]);
return { user, posts, comments };
}
- 순차: 300ms + 200ms + 150ms = 650ms
- 병렬: max(300ms, 200ms, 150ms) = 300ms
Subset 활용 최적화
상황에 따라 적절한 Subset을 선택합니다.복사
// 목록 화면: 기본 정보만
const users = await UserService.getUsers("A");
users.map((user) => (
<div key={user.id}>
{user.username} - {user.email}
</div>
));
// 상세 화면: 전체 정보
const fullUser = await UserService.getUser("C", userId);
<div>
<h1>{fullUser.username}</h1>
<p>{fullUser.bio}</p>
<p>Created: {fullUser.createdAt}</p>
</div>
TanStack Query 활용
조건부 페칭
복사
function UserProfile({ userId }: { userId: number | null }) {
const { data: user } = UserService.useUser(
"A",
userId!,
{ enabled: userId !== null } // userId가 null이면 호출 안함
);
if (!userId) return <div>Please select a user</div>;
if (!user) return <div>Loading...</div>;
return <div>{user.username}</div>;
}
캐시 무효화
복사
import { useQueryClient } from "@tanstack/react-query";
import { UserService } from "@/services/services.generated";
function EditProfile({ userId }: { userId: number }) {
const queryClient = useQueryClient();
async function handleUpdate(data: any) {
await UserService.updateProfile(data);
// 특정 쿼리 무효화
queryClient.invalidateQueries(
UserService.getUserQueryOptions("A", userId)
);
// 또는 모든 User 쿼리 무효화
queryClient.invalidateQueries({
queryKey: ["User"],
});
}
}
Prefetching
복사
function UserList({ userIds }: { userIds: number[] }) {
const queryClient = useQueryClient();
// 호버 시 미리 로드
function handleMouseEnter(userId: number) {
queryClient.prefetchQuery(
UserService.getUserQueryOptions("A", userId)
);
}
return (
<ul>
{userIds.map((id) => (
<li
key={id}
onMouseEnter={() => handleMouseEnter(id)}
>
User {id}
</li>
))}
</ul>
);
}
실전 예제
전체 CRUD 플로우
복사
import { useState } from "react";
import { UserService } from "@/services/services.generated";
import { isSonamuError } from "@/lib/sonamu.shared";
function UserManagement() {
// 목록 조회 (Subset A)
const { data: users, refetch } = UserService.useUsers("A");
// 생성
async function handleCreate(data: { username: string; email: string }) {
try {
await UserService.create(data);
refetch(); // 목록 새로고침
alert("Created!");
} catch (error) {
if (isSonamuError(error)) {
alert(error.message);
}
}
}
// 수정
async function handleUpdate(id: number, data: { username: string }) {
try {
await UserService.update(id, data);
refetch();
alert("Updated!");
} catch (error) {
if (isSonamuError(error)) {
alert(error.message);
}
}
}
// 삭제
async function handleDelete(id: number) {
if (!confirm("Are you sure?")) return;
try {
await UserService.delete(id);
refetch();
alert("Deleted!");
} catch (error) {
if (isSonamuError(error)) {
alert(error.message);
}
}
}
return (
<div>
<h1>Users</h1>
{users?.map((user) => (
<div key={user.id}>
{user.username}
<button onClick={() => handleUpdate(user.id, { username: "new" })}>
Edit
</button>
<button onClick={() => handleDelete(user.id)}>
Delete
</button>
</div>
))}
</div>
);
}
주의사항
Service 사용 시 주의사항:
- 생성된 Service 파일(
services.generated.ts)은 절대 수동 수정 금지 - Subset 파라미터 필수: getUser 등에서 subset 지정 필요
- Server Components에서는 Service 대신 백엔드 모델 직접 호출
- 에러 처리 시
isSonamuError()타입 가드 사용 - TanStack Query Hook은 컴포넌트 내부에서만 호출
await키워드 필수 (모든 Service 함수는 async)- Namespace이므로 new 불필요:
UserService.getUser()직접 호출
