aurora10분
서버 가입 신청 대기 목록 조회 기능
2025년 8월 27일
Aurora 프로젝트 - 훅 아키텍처 리팩토링 & UI 개선
기존 채널/프로젝트 관리 시스템 구현 이후, 코드의 복잡성이 증가하면서 유지보수성 문제가 생겼다. 특히 useServer.ts 훅이 너무 많은 책임을 가지게 되어 단일 책임 원칙을 위반하고 있었고, pending 페이지의 디자인도 개선이 필요한 상황이었다.
마주한 아키텍처 문제들
1. useServer 훅의 비대화 문제
useServer.ts 훅이 서버 추가, 접근 권한 확인, 프로젝트 목록 조회, 채널 목록 조회, 기본 채널 생성 등 너무 많은 역할을 담당하고 있었다.
typescript// 문제가 된 코드 - 하나의 훅에 모든 로직이 집중
export const useServer = () => {
// 서버 추가 관련 로직
const addServerMutation = useAddServerMutation();
// 서버 접근 권한 확인 로직
const getServerListMutation = useGetServerListMutation();
// 프로젝트 목록 조회 로직
const getProjectListMutation = useGetProjectListMutation();
// 채널 목록 조회 로직
const getChannelListMutation = useGetChannelListMutation();
// 기본 채널 생성 로직
const createChannelMutation = useCreateChannelMutation();
// 복잡한 handleGetProjectList 함수 (100줄+)
const handleGetProjectList = async (serverUrl: string, serverName: string) => {
// 서버 목록 확인 -> 프로젝트 조회 -> 채널 조회 -> 기본 채널 생성 -> 라우팅
// 모든 로직이 한 함수에...
};
};
문제점들:
- 단일 책임 원칙 위반
- 테스트하기 어려운 구조
- 재사용성 부족
- 의존성 관리 복잡
해결책: 단일 책임 원칙에 따른 훅 분리
1. useServerConnection - 서버 접근 권한 확인 전담
typescript// frontend/src/app/(server-setup)/hooks/useServerConnection.ts
export const useServerConnection = () => {
const getServerListMutation = useGetServerListMutation();
const router = useRouter();
/**
* 서버 접근 권한 확인
*/
const validateServerAccess = async (
serverUrl: string,
serverName: string
): Promise<boolean> => {
try {
console.log("🔍 사용자 서버 목록 조회 중...");
const serverList = await getServerListMutation.mutateAsync();
const isServerAvailable = serverList.some(
(server) =>
server.serverUrl === serverUrl || server.serverName === serverName
);
if (!isServerAvailable) {
console.log("❌ 서버에 접근 권한이 없습니다. 승인 대기 페이지로 이동합니다.");
router.push(
`/pending?serverUrl=${encodeURIComponent(serverUrl)}&serverName=${encodeURIComponent(serverName)}`
);
return false;
}
console.log("✅ 서버 접근 권한 확인됨.");
return true;
} catch (error) {
console.error("❌ 서버 접근 권한 확인 실패:", error);
throw error;
}
};
return {
validateServerAccess,
isValidating: getServerListMutation.isPending,
validationError: getServerListMutation.error,
};
};
2. useProjectNavigation - 프로젝트/채널 로직 전담
typescript// frontend/src/app/(server-setup)/hooks/useProjectNavigation.ts
export const useProjectNavigation = () => {
const getProjectListMutation = useGetProjectListMutation();
const getChannelListMutation = useGetChannelListMutation();
const createChannelMutation = useCreateChannelMutation();
const router = useRouter();
const navigateToFirstChannel = async (serverUrl: string, serverName: string): Promise<void> => {
try {
// 1. 프로젝트 목록 조회
console.log("📋 프로젝트 목록 조회 중...");
const projectList = await getProjectListMutation.mutateAsync({ serverUrl });
if (!projectList || projectList.length === 0) {
throw new Error("프로젝트 목록이 비어있습니다.");
}
const firstProject = projectList[0];
console.log("✅ 첫 번째 프로젝트:", firstProject);
// 2. 채널 목록 조회
console.log("📺 채널 목록 조회 중...");
let channelList = await getChannelListMutation.mutateAsync({
serverUrl,
projectPk: firstProject.projectPk,
});
// 3. 채널이 없으면 기본 채널 생성
if (!channelList || channelList.length === 0) {
console.log("🔨 기본 'general' 채널 생성 중...");
const newChannel = await createChannelMutation.mutateAsync({
serverUrl,
projectPk: firstProject.projectPk,
channelData: {
channelName: "general",
channelKind: "text",
isPrivate: false,
channelRole: "member",
},
});
channelList = [newChannel];
}
// 4. 서버 정보 저장
const serverInfo: ServerInfo = { serverUrl, serverName };
sessionStorage.setItem("currentServerInfo", JSON.stringify(serverInfo));
// 5. 첫 번째 채널로 라우팅
const targetChannel = channelList[0];
const encodedChannelName = encodeURIComponent(targetChannel.channelName);
const targetUrl = `/${serverUrl}/projects/${firstProject.projectPk}/channels/${encodedChannelName}`;
console.log("🚀 라우팅:", targetUrl);
router.push(targetUrl);
} catch (error) {
console.error("❌ 프로젝트 탐색 실패:", error);
throw error;
}
};
return {
navigateToFirstChannel,
isLoadingProjects: getProjectListMutation.isPending,
isLoadingChannels: getChannelListMutation.isPending,
isCreatingChannel: createChannelMutation.isPending,
projectError: getProjectListMutation.error,
channelError: getChannelListMutation.error,
createChannelError: createChannelMutation.error,
};
};
3. useServerFlow - 전체 플로우 관리
typescript// frontend/src/app/(server-setup)/hooks/useServerFlow.ts
export const useServerFlow = () => {
const addServerMutation = useAddServerMutation();
const { validateServerAccess, isValidating, validationError } = useServerConnection();
const {
navigateToFirstChannel, isLoadingProjects, isLoadingChannels, isCreatingChannel,
projectError, channelError, createChannelError,
} = useProjectNavigation();
const handleAddServer = async (data: ServerRequest) => {
try {
await addServerMutation.mutateAsync(data);
} catch (error) {
console.error("서버 추가 실패:", error);
throw error;
}
};
const handleServerConnection = async (serverUrl: string, serverName: string) => {
// 1단계: 서버 접근 권한 확인
const hasAccess = await validateServerAccess(serverUrl, serverName);
if (!hasAccess) return; // pending 페이지로 리다이렉트됨
// 2단계: 프로젝트/채널 탐색 및 입장
await navigateToFirstChannel(serverUrl, serverName);
};
// 로딩 상태 통합
const isLoading = isValidating || isLoadingProjects || isLoadingChannels || isCreatingChannel;
const hasError = validationError || projectError || channelError || createChannelError;
return {
handleAddServer,
handleServerConnection,
isLoading,
hasError,
// 개별 상태들
isAddingServer: addServerMutation.isPending,
isValidatingAccess: isValidating,
isLoadingProjects,
isLoadingChannels,
isCreatingChannel,
// 에러들
addServerError: addServerMutation.error,
validationError,
projectError,
channelError,
createChannelError,
// 기타
isAddServerSuccess: addServerMutation.isSuccess,
resetAddServer: addServerMutation.reset,
};
};
4. useServer - 호환성 유지를 위한 래퍼
typescript// frontend/src/app/(server-setup)/hooks/useServer.ts
export const useServer = () => {
const serverFlow = useServerFlow();
// 기존 인터페이스와 호환성 유지
return {
handleAddServer: serverFlow.handleAddServer,
handleGetProjectList: serverFlow.handleServerConnection, // 이름만 매핑
isAddingServer: serverFlow.isAddingServer,
isGettingServerList: serverFlow.isValidatingAccess,
isGettingProjectList: serverFlow.isLoadingProjects,
isGettingChannelList: serverFlow.isLoadingChannels,
isCreatingChannel: serverFlow.isCreatingChannel,
isAddServerSuccess: serverFlow.isAddServerSuccess,
isAddServerError: !!serverFlow.addServerError,
addServerError: serverFlow.addServerError,
serverListError: serverFlow.validationError,
projectListError: serverFlow.projectError,
channelListError: serverFlow.channelError,
createChannelError: serverFlow.createChannelError,
resetAddServer: serverFlow.resetAddServer,
};
};
컴포넌트별 마이그레이션
1. AddServerModal 마이그레이션
typescript// Before
import { useServer } from "../hooks/useServer";
const { handleAddServer, isAddingServer, isAddServerSuccess, isAddServerError } = useServer();
// After
import { useServerFlow } from "../hooks/useServerFlow";
const { handleAddServer, isAddingServer, isAddServerSuccess, addServerError } = useServerFlow();
const isAddServerError = !!addServerError; // 에러 상태 계산
2. server-connect 페이지 마이그레이션
typescript// Before
const {
handleGetProjectList,
isGettingServerList,
isGettingProjectList,
serverListError,
projectListError,
} = useServer();
// After
const {
handleServerConnection,
isValidatingAccess,
isLoadingProjects,
validationError,
projectError,
} = useServerFlow();
Pending 페이지 디자인 개선
문제 상황
기존 pending 페이지가 (server-setup) layout의 제약을 받아 작은 카드 형태로만 표시되어 사용자 경험이 좋지 않았다.
typescript// 문제: layout이 모든 페이지에 동일하게 적용
<div className="bg-aurora-form/62 rounded-xl p-8 shadow-2xl border border-white/10">
{children} {/* pending 페이지도 작은 카드 안에 제한됨 */}
</div>
해결방법: 조건부 Layout 렌더링
typescript// frontend/src/app/(server-setup)/layout.tsx
const ServerSetupLayout = ({ children }: { children: React.ReactNode }) => {
const { handleLogout } = useAuth();
const pathname = usePathname();
// pending 페이지일 때는 전체 화면 레이아웃 사용
const isPendingPage = pathname.includes('/pending');
if (isPendingPage) {
return <div className="min-h-screen">{children}</div>;
}
// 기존 layout 코드...
return (
<div className="min-h-screen flex relative bg-aurora-blue-gradient-diagonal">
{/* ... */}
</div>
);
};
스마트폰 형태 디자인 구현
typescript// frontend/src/app/(server-setup)/pending/page.tsx
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex">
{/* 왼쪽: Aurora 로고 */}
<div className="flex-1 flex items-center justify-center relative">
<motion.div className="relative z-10 text-center">
<h1 className="text-7xl font-bold text-white tracking-wide">Aurora</h1>
<div className="w-24 h-1 bg-gradient-to-r from-purple-500 to-blue-500 mx-auto mt-4 rounded"></div>
</motion.div>
</div>
{/* 오른쪽: 스마트폰 형태 컨텐츠 */}
<div className="flex-1 flex items-center justify-center p-8">
<div className="bg-slate-800/80 backdrop-blur-sm rounded-3xl p-1 shadow-2xl border border-slate-700/50">
<div className="bg-slate-900 rounded-3xl p-6 min-h-[600px] flex flex-col">
{/* 상단 카드: 서버 가입 승인 대기 */}
<motion.div className="bg-slate-800/60 rounded-2xl p-6 mb-6 border border-slate-700/30">
<h2 className="text-xl font-semibold text-white text-center mb-4">
서버 가입 승인 대기
</h2>
{/* 상태 표시 */}
<div className="bg-blue-500/20 border border-blue-500/30 rounded-xl p-3">
<div className="flex items-center justify-center space-x-2">
<div className={`w-2 h-2 bg-blue-400 rounded-full ${approvalStatus === "pending" ? "animate-pulse" : ""}`}></div>
<span className="text-white text-sm font-medium">
{statusConfig.statusText}
</span>
</div>
</div>
</motion.div>
{/* 하단 카드: 서버 가입 액션 */}
<motion.div className="bg-slate-800/60 rounded-2xl p-6 border border-slate-700/30 flex-1 flex flex-col">
<h3 className="text-lg font-semibold text-white mb-4">서버 가입</h3>
<div className="space-y-3">
<button className="w-full bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 text-white font-medium py-3 px-4 rounded-xl transition-all duration-200">
승인 상태 확인
</button>
<button className="w-full bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-medium py-3 px-4 rounded-xl transition-all duration-200">
최근 서버 둘아가기
</button>
</div>
</motion.div>
</div>
</div>
</div>
</div>
);
🎉 최종 결과
아키텍처 개선 효과
| 항목 | Before | After |
|---|---|---|
| 코드 라인 수 | useServer.ts 200+ 줄 | 4개 파일로 분산 (각 50-80줄) |
| 책임 분리 | ❌ 모든 로직이 한 곳에 | ✅ 단일 책임 원칙 준수 |
| 재사용성 | ❌ 전체를 가져와야 함 | ✅ 필요한 부분만 사용 가능 |
| 테스트 용이성 | ❌ 복잡한 의존성 | ✅ 독립적인 단위 테스트 가능 |
| 유지보수성 | ❌ 수정 시 사이드 이펙트 위험 | ✅ 격리된 수정 가능 |
컴포넌트별 변경 완료
- ✅ AddServerModal:
useServer→useServerFlow - ✅ server-connect/page: 상태명 매핑 완료
- ✅ pending/page: 새로운
useServerFlow사용 - ✅ 하위 호환성: 기존
useServer인터페이스 유지
UI/UX 개선
- ✅ 전체 화면 활용: layout 제약 제거
- ✅ 스마트폰 형태 디자인: 모던한 모바일 UI
- ✅ 좌우 분할 레이아웃: Aurora 로고 + 컨텐츠 영역
- ✅ 상하 카드 구조: 정보 표시 + 액션 영역 분리
- ✅ 반응형 애니메이션: Framer Motion 활용
구현된 아키텍처 패턴
plainuseServerFlow (Orchestrator) ├── useServerConnection (서버 접근 권한) ├── useProjectNavigation (프로젝트/채널 탐색) └── useAddServerMutation (서버 추가) useServer (Compatibility Layer) └── useServerFlow 래핑하여 기존 인터페이스 유지
음 원래 하던대로 훅 하나씩 계속 만들어서 할 걸.. 괜히 하나의 훅에 여러 기능 연결해보려다가 낭패를 본 것 같다.. 다른 훅도 이렇게 되어 있으면 바꿔야겠다..
태그
#Next.js#aurora
