서버 연결, 프로젝트, 채널 조회
Aurora 프로젝트 - 서버 연결 시스템 구현 완료
로그인, 회원 가입 로직 이후 바로 서버 연결 기능을 구현하기 시작했다. API 통신에는 이전에 만들었던 라이브러리를 활용했고, 서버 데이터 캐싱은 TanStack-Query를 활용해서 진행했다.
처음 마주한 문제들
1. 서버 추가 실패 시 피드백 없음
처음에는 서버 연결 시 message 가 존재하는으로 확인을 하는데 실패의 경우 message 에 서버 추가 실패를 적어줬었다. 그러다보니 실패한 경우에도 메시지가 존재하여 실제로는 서버 생성이 되지 않았음에도 보여지는 화면에서는 실제로 생성 된 것 처럼 나오게 되었다.
해결방법: 실패 전용 모달창 추가하고 구체적인 에러 메시지 표시하도록 구현
typescript{isAddServerError && (
<div className="flex flex-col space-y-4">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold text-red-400 mb-2">
서버 추가 실패
</h2>
<p className="text-white/80 mb-4">
서버 추가 중 오류가 발생했습니다.
</p>
{addServerError && (
<div className="bg-red-500/20 border border-red-500/30 rounded-lg p-3 mb-4">
<p className="text-red-300 text-sm">
{addServerError instanceof Error
? addServerError.message
: "알 수 없는 오류가 발생했습니다."}
</p>
</div>
)}
</div>
</div>
)}
2. useApi에서 동적 endpoint 처리 못함
typescript// 이런 식으로 하려고 했는데...
const {
execute: getProjectListApi,
loading: isGettingProjectList,
error: getProjectListError,
} = useApi<Project[], void>({
endpoint: `/ex/${serverUrl}/projects`, // serverUrl이 정의 안됨!
method: "GET",
axiosInstance: expressClient,
});
serverUrl 변수가 정의되지 않아서 에러가 계속 발생했다.
해결방법: useCallback으로 메모화된 함수 생성해서 동적 endpoint 지원
typescriptconst getProjectList = useCallback(async (serverUrl: string): Promise<Project[]> => {
setProjectListLoading(true);
setProjectListError(null);
try {
const response = await expressClient.get<Project[]>(`/ex/servers/${serverUrl}/projects`);
setProjectListLoading(false);
return response.data || [];
} catch (error) {
console.error("프로젝트 목록 조회 실패:", error);
setProjectListError(error as Error);
setProjectListLoading(false);
throw error;
}
}, []);
서버 연결 자동화 시스템 구현
useServer.ts
typescript/**
* 프로젝트 목록 조회 후 자동으로 채널 입장 처리
*/
const handleGetProjectList = async (serverUrl: string, serverName: string) => {
try {
console.log("🚀 서버 연결 프로세스 시작:", { serverUrl, serverName });
// 1. 프로젝트 목록 조회
console.log("📋 프로젝트 목록 조회 중...");
const projects = await getProjectListMutation.mutateAsync(serverUrl);
if (!projects || projects.length === 0) {
throw new Error("프로젝트가 존재하지 않습니다.");
}
// 2. 첫 번째 프로젝트 선택
const firstProject = projects[0];
console.log("📁 첫 번째 프로젝트 선택:", firstProject);
// 3. 해당 프로젝트의 채널 목록 조회
console.log("📺 채널 목록 조회 중...");
const channels = await getChannelListMutation.mutateAsync({
serverUrl,
projectPk: firstProject.projectPk
});
let targetChannel;
// 4. 채널이 비어있는지 확인
if (!channels || channels.length === 0) {
console.log("📺 채널이 없어서 기본 채널 생성 중...");
// 5. 채널 생성
targetChannel = await createChannelMutation.mutateAsync({
serverUrl,
projectPk: firstProject.projectPk,
channelData: {
channelKind: "text",
isPrivate: false,
channelRole: "member"
}
});
console.log("✅ 기본 채널 생성 완료:", targetChannel);
} else {
// 6. 첫 번째 채널 선택
targetChannel = channels[0];
console.log("📺 첫 번째 채널 선택:", targetChannel);
}
// 7. 서버 정보를 sessionStorage에 저장하고 라우팅
const serverInfo: ServerInfo = {
serverName,
serverUrl,
projectName: firstProject.projectName,
projectPk: firstProject.projectPk,
channelName: targetChannel.channelName,
};
// sessionStorage에 서버 정보 저장
if (typeof window !== "undefined") {
sessionStorage.setItem("currentServerInfo", JSON.stringify(serverInfo));
}
const targetUrl = `/${serverUrl}/projects/${firstProject.projectName}/channels/${targetChannel.channelName}`;
router.push(targetUrl);
console.log("🎉 서버 연결 완료!");
} catch (error) {
console.error("❌ 서버 연결 실패:", error);
throw error;
}
};
이제 사용자가 서버 URL이랑 이름만 입력하면 자동으로:
- 프로젝트 목록 조회
- 첫 번째 프로젝트 선택
- 채널 목록 조회
- 채널 없으면 기본 채널 생성, 있으면 첫 번째 채널 선택
- 적절한 URL로 자동 라우팅
이 모든 과정이 자동으로 진행된다!
실시간 로딩 상태 표시
typescript// 로딩 상태 표시
{isLoading && (
<div className="text-center text-white/80 text-sm">
{isGettingProjectList && "📋 프로젝트 목록 조회 중..."}
{isGettingChannelList && "📺 채널 목록 조회 중..."}
{isCreatingChannel && "🔨 기본 채널 생성 중..."}
</div>
)}
<button
type="submit"
disabled={isLoading}
className="w-full bg-purple-500 hover:bg-purple-600 disabled:bg-purple-400 disabled:cursor-not-allowed text-white font-semibold py-3 px-4 rounded-lg transition duration-200 focus:outline-none focus:ring-2 focus:ring-purple-400"
>
{isLoading ? "연결 중..." : "서버 입장"}
</button>
사용자가 어떤 단계에서 기다리고 있는지 명확하게 알 수 있도록 단계별 로딩 메시지를 표시했다.
무한 API 요청 지옥에서 탈출

image.png
음.. 이번에도 useEffect다.. 네트워크 탭을 보니까 API 요청이 계속 무한으로 나가고 있는 상황이었다.
문제 원인: useEffect의 의존성 배열에 함수가 포함되어 있는데, 이 함수들이 매번 새로 생성되어서 무한 루프 발생
typescript// 문제가 된 코드
useEffect(() => {
loadProjects();
}, [serverInfo?.serverUrl, projectId, getProjectList]); // 함수가 의존성에 포함!
해결방법:
useCallback으로 함수 메모화- 의존성 배열에서 함수 제거하고 실제 데이터만 포함
typescript// useServerApi.ts - 함수 메모화
const getProjectList = useCallback(async (serverUrl: string): Promise<Project[]> => {
// ... 로직
}, []); // 빈 의존성 배열로 한 번만 생성
// ProjectSidebar.tsx - 의존성 정리
useEffect(() => {
loadProjects();
}, [serverInfo?.serverUrl, projectId]); // 함수 제거, 실제 데이터만
이렇게 하니까 무한 요청이 멈추고 필요할 때만 API가 호출되었다.
sessionStorage로 상태 관리
URL에 서버 정보를 넣는 것보다 sessionStorage를 사용하는 게 더 깔끔하다고 판단했다.
typescript// 서버 정보 타입 정의
export interface ServerInfo {
serverName: string; // 사용자가 입력한 서버 이름
serverUrl: string; // 서버 도메인
projectName: string; // API에서 받은 프로젝트 이름
projectPk: number; // 프로젝트 식별자
channelName: string; // 입장한 채널 이름
}
// 서버 정보 저장
if (typeof window !== "undefined") {
sessionStorage.setItem("currentServerInfo", JSON.stringify(serverInfo));
}
// 서버 정보 가져오기 훅
export const useCurrentServerInfo = (): ServerInfo | null => {
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
useEffect(() => {
if (typeof window !== 'undefined') {
const stored = sessionStorage.getItem('currentServerInfo');
if (stored) {
try {
setServerInfo(JSON.parse(stored));
} catch (error) {
console.error('서버 정보 파싱 실패:', error);
setServerInfo(null);
}
}
}
}, []);
return serverInfo;
};
이제 어느 컴포넌트에서든 useCurrentServerInfo() 훅으로 현재 서버 정보를 쉽게 가져올 수 있다.
하드코딩된 데이터를 실제 API 데이터로 교체
ServerHeader.tsx
typescript// 이전: 하드코딩된 함수 사용
{getServerName(serverId)}
{getChannelName(channelId)}
// 현재: 실제 서버 정보 사용
const serverInfo = useCurrentServerInfo();
const serverName = serverInfo?.serverName || "서버";
const channelName = serverInfo?.channelName || "채널";
ProjectSidebar.tsx
typescript// 이전: 하드코딩된 데이터
import { projects, channels } from "../types/data";
// 현재: 실제 API 데이터
const [projects, setProjects] = useState<Project[]>([]);
const [channels, setChannels] = useState<Channel[]>([]);
// API에서 데이터 로딩
useEffect(() => {
const loadProjects = async () => {
if (!serverInfo?.serverUrl) return;
setLoadingProjects(true);
try {
const projectList = await getProjectList(serverInfo.serverUrl);
setProjects(projectList);
} catch (error) {
console.error("프로젝트 목록 로딩 실패:", error);
} finally {
setLoadingProjects(false);
}
};
loadProjects();
}, [serverInfo?.serverUrl, projectId]);
useChannelPage.ts
typescript// 이전: 하드코딩된 환영 메시지
const [messages, setMessages] = useState<Message[]>(defaultMessages);
// 현재: 실제 채널 정보 기반 환영 메시지
if (channel) {
const welcomeMessages: Message[] = [
{
id: 1,
user: "시스템",
content: `${channel.channelName} 채널에 오신 것을 환영합니다!`,
timestamp: new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
hour12: true,
}),
isSystem: true,
},
{
id: 2,
user: "시스템",
content: `이 채널은 ${channel.isPrivate ? '비공개' : '공개'} 채널입니다.`,
timestamp: new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
hour12: true,
}),
isSystem: true,
}
];
setMessages(welcomeMessages);
}
React Query mutation 타입 에러 해결
typescript// 문제: 여러 파라미터 전달 시 타입 에러
mutationFn: async (serverUrl: string, projectPk: number) => {
// React Query는 단일 파라미터만 받음!
}
// 해결: 객체로 묶어서 전달
mutationFn: async ({ serverUrl, projectPk }: { serverUrl: string; projectPk: number }) => {
const result = await getChannelList(serverUrl, projectPk);
return result;
}
// 사용 시
channelMutation.mutate({
serverUrl: "your-server-url",
projectPk: 123
});
🎉 최종 결과

image.png

image.png

image.png

image.png
이제 사용자 경험이 완전히 바뀌었다:
- 서버 URL/이름 입력 → "서버 입장" 클릭
- 단계별 진행 상황 확인 → "📋 프로젝트 목록 조회 중..."
- 자동으로 적절한 채널 입장
- 실제 서버 정보로 UI 업데이트 → 가짜 이름 사라짐
TanStack-Query랑 내가 만든 라이브러리 연동하는 것도 생각보다 복잡했다. 특히 React Query의 mutation이 단일 파라미터만 받는다는 제약 때문에 객체로 감싸서 전달해야 하는 부분이 좀 헷갈렸다. 이전에 했던 거 지만 다시 학습을 좀 더 해야겠다…
이제 사용자 초대 프로젝트, 채널 생성 바빠질 일 만 남았다..
