background
aurora10분

서버 연결, 프로젝트, 채널 조회

2025년 8월 20일

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 지원

typescript
const 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이랑 이름만 입력하면 자동으로:

  1. 프로젝트 목록 조회
  2. 첫 번째 프로젝트 선택
  3. 채널 목록 조회
  4. 채널 없으면 기본 채널 생성, 있으면 첫 번째 채널 선택
  5. 적절한 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

image.png

음.. 이번에도 useEffect다.. 네트워크 탭을 보니까 API 요청이 계속 무한으로 나가고 있는 상황이었다.

문제 원인: useEffect의 의존성 배열에 함수가 포함되어 있는데, 이 함수들이 매번 새로 생성되어서 무한 루프 발생

typescript
// 문제가 된 코드
useEffect(() => {
  loadProjects();
}, [serverInfo?.serverUrl, projectId, getProjectList]); // 함수가 의존성에 포함!

해결방법:

  1. useCallback으로 함수 메모화
  2. 의존성 배열에서 함수 제거하고 실제 데이터만 포함
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

image.png

image.png

image.png

image.png

이제 사용자 경험이 완전히 바뀌었다:

  1. 서버 URL/이름 입력 → "서버 입장" 클릭
  2. 단계별 진행 상황 확인 → "📋 프로젝트 목록 조회 중..."
  3. 자동으로 적절한 채널 입장
  4. 실제 서버 정보로 UI 업데이트 → 가짜 이름 사라짐

TanStack-Query랑 내가 만든 라이브러리 연동하는 것도 생각보다 복잡했다. 특히 React Query의 mutation이 단일 파라미터만 받는다는 제약 때문에 객체로 감싸서 전달해야 하는 부분이 좀 헷갈렸다. 이전에 했던 거 지만 다시 학습을 좀 더 해야겠다…

이제 사용자 초대 프로젝트, 채널 생성 바빠질 일 만 남았다..

태그

#Next.js#aurora