IAM 을 회사에서 구축하며, 각 서비스 개발자들이 IAM을 연동하는데 도움을 주는 admin UI를 개발 하고 있다.
Next.js 를 사용해서 개발하고 있는데,
그냥 코드를 짜도 동작은 하지만,
모듈 분리를 통해 유지보수를 수월하도록 변경했다.
💫 모듈 분리를 하는 이유
✅ 유지보수성 향상
한 파일에 모든 기능이 몰려 있으면 수정할 때 영향도가 커지고, 실수할 확률도 높아집니다. 모듈을 분리하면 코드 수정 범위를 줄일 수 있어 안정성이 증가합니다.
✅ 재사용성 증가
API 요청, 유틸 함수, 검증 로직 등을 모듈화하면 여러 곳에서 재사용할 수 있어 코드 중복을 방지할 수 있습니다.
✅ 협업 효율성 향상
여러 명이 동시에 작업할 때, 코드가 깔끔하게 정리되어 있으면 충돌을 줄이고 작업 분배가 쉬워집니다.
✅ 성능 최적화
불필요한 코드 로딩을 방지할 수 있고, 컴포넌트 단위로 코드 스플리팅이 가능해 빌드 및 실행 속도를 최적화할 수 있습니다
📂디렉토리 구조 정리하기
/src
├── app # Next.js의 App Router (페이지 라우팅)
├── components # 공통 UI 컴포넌트 모음
├── lib # 공통 로직, API, 유틸리티 함수 관리
│ ├── api # API 요청 관련 모듈
│ ├── utils # 공통적으로 사용되는 유틸리티 함수
│ ├── schemas # 데이터 검증을 위한 스키마 정의
│ ├── services # 비즈니스 로직을 처리하는 서비스 계층
├── hooks # 커스텀 훅 모음
├── types # TypeScript 타입 정의 파일
├── styles # 전역 스타일 및 Tailwind 설정
├── public # 정적 파일 (이미지, 아이콘 등)
API 분리
API 요청을 각 컴포넌트에서 직접 처리하면 코드가 중복되고 관리가 어려워집니다. 이를 해결하려면 API 요청을 별도의 모듈로 분리하는 것이 좋습니다.
📌 API 분리 예시 (lib/api/user.ts)
export const fetchUsers = async () => {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
return response.json();
};
이제 API를 호출할 때 단순히 다음과 같이 사용할 수 있습니다.
import { fetchUsers } from "@/lib/api/user";
useEffect(() => {
fetchUsers().then(setUsers).catch(console.error);
}, []);
이렇게 하면 API 요청을 한 곳에서 관리할 수 있어 유지보수가 쉬워지고, 필요하면 공통 로직을 추가하여 코드 중복을 줄일 수 있습니다.
Schema 분리하기 (Zod 활용)
데이터 검증을 위해 Zod를 사용하는 경우가 많습니다. 검증 로직을 따로 분리하면 API 요청 또는 Form 검증 시 일관된 유효성 검사를 적용할 수 있습니다.
📌 Schema 분리 예시 (lib/schemas/user.ts)
import { z } from "zod";
export const UserSchema = z.object({
id: z.string(),
email: z.string().email("유효한 이메일을 입력하세요."),
name: z.string().min(2, "이름은 최소 2글자 이상이어야 합니다."),
});
이제 API 요청 시에도 동일한 검증을 적용할 수 있습니다.
import { UserSchema } from "@/lib/schemas/user";
const validateUser = (user: any) => {
return UserSchema.safeParse(user);
};
Schema를 분리하면 데이터 검증을 중앙에서 관리할 수 있어 일관성을 유지할 수 있습니다.
index.ts를 활용한 모듈 관리
각 모듈에서 index.ts 파일을 활용하면 import 경로를 짧게 만들고 가독성을 높일 수 있습니다.
📌 index.ts 활용 예시 (lib/api/index.ts)
export * from "./user";
export * from "./client";
이제 API 관련 모듈을 다음과 같이 불러올 수 있습니다.
import { fetchUsers } from "@/lib/api";
이 방식은 API뿐만 아니라 유틸 함수, 타입, 컴포넌트 모듈화에서도 유용하게 사용할 수 있습니다.
types와 services 분리
📌 types 분리 (types/hydra.ts)
export type OAuthClientConfiguration = {
client_id: string;
client_name: string;
client_secret: string;
redirect_uris: string[];
grant_types: string[];
scope: string;
token_endpoint_auth_method: string;
access_token_lifespan: string;
refresh_token_lifespan: string;
skip_consent: boolean;
post_logout_redirect_uris: string[];
};
📌 services 분리 (lib/services/clientService.ts)
import { fetchClient, patchClient } from "@/lib/api/hydra";
export const getClientDetails = async (id: string) => {
return await fetchClient(id);
};
export const updateClient = async (id: string, changes: any) => {
return await patchClient(id, changes);
};
이렇게 분리하면 API 호출과 비즈니스 로직을 명확히 나눌 수 있어 유지보수성이 향상됩니다.

유지보수를 쉽게 하기 위한 코드 리팩토링 과정은 정리 정돈과 비슷하다고 생각한다.
나중에 한꺼번에 정리하려고 하면 감당하기 어려워지지만,
눈에 보이는 부분부터 하나씩 정리하다 보면 어느 순간 유지보수하기 좋은 깔끔한 코드가 완성된다.
처음에는 구현이 급해서 모든 기능을 한 파일에 몰아넣었지만,
기능별로 분리하고 정리한 후에는 새로운 기능을 추가할 때 확실히 더 편해지는 걸 느낄 수 있었다.
앞으로도 항상 클린 코드를 작성하는 습관을 길러야겠다고 다짐하게 된다.
'Frontend · UI' 카테고리의 다른 글
wsl2로 rocky9 환경으로 react 프로젝트 만들기 (0) | 2025.03.06 |
---|