시작하며…

토이프로젝트2 기간인 2주가 순식간에 지나갔다. 토이프로젝트1은 상대적으로 여유롭게 진행되었지만 토이프로젝트2는 제출일까지 꽉 채워야할만큼 바쁜 일정이었다. 새벽까지 작업하고 오프라인으로도 하루 종일 작업하는 날이 많아서 금요일날 미니프로젝트 빌드업 미팅이 끝나자마자 곯아떨어지고 토요일에도 하루종일 잠만 잤던것같다. 그래도 이제 정신 차리고 기억이 남아있을 때 토이프로젝트2에 대한 회고를 작성해보려고한다.

팀빌딩 미팅

이번에는 토이프로젝트1 때와 달리 월요일날 팀빌딩미팅을 했다. 지금 생각해보니 금요일과 주말을 통해 기획, 디자인을 미리 하고 개발을 시작했으면 이렇게 빡빡한 일정이 안 되었을 수도 있겠다는 생각이 들기도 했지만, 다수결에 의해 결정된 일정이다보니 어쩔 수 없었다.

우리 팀은 민섭이, 은지네 스터디원이었던 승준님과 문용이, 그룹장 미팅때 잠간 뵀던 승연님, 처음 뵙는 경규님, 그리고 나 이렇게 구성되어 있었다. 아는 사람이 아무도 없어서 처음에는 엄청 어색한 분위기였다. 팀빌딩 미팅 todo부터 작성을 시작했다.

먼저, 팀 이름을 정하는 시간이 있었는데, 후보로는 문용이가 냈던 “내 통장에 2조”와 내가 냈던 “I am 2조에요~” 가 있었다. 투표로 팀명을 골랐는데, 2:3으로 “I am 2조에요~”가 뽑혔다 ㅋㅋ.

image

위 사진처럼 승준님이 팀명에 걸맞는 노션 프로필 사진을 야무지게 편집해주셨다.

팀장은 승준님이 희망하셔서 승준님으로 결정되었다.

이어서 프로젝트 컨셉 회의를 진행하였다. 언어 교환 어플부터 면접 연습까지 다양한 아이디어가 나오다가 내가 캐치마인드라는 아이디어를 제시했다. 하지만 점수 관리, 그림 그리는 권한 등 로직 구현이 어렵고 2주 안에 별도의 서버를 구축해서 결과물을 내기에는 어려워 보였다. 이어서 경규님이 음악 퀴즈를 제시해주셨는데 관리자가 지정한 음악 외에는 등록하기가 힘들고 캐치마인드와 비슷한 이유로 시간상 포기하기로 했다. 최종적으로 마지막에 나온 아이디어인 “호그와트 기숙사별 채팅방” 을 구현해보기로 했다. 처음에는 특정 키워드(ex) 마법 주문)을 입력 시 모션을 추가하거나 볼드모트를 퇴치할 수 있는 기믹을 구상했지만, 나중에 시간상 모두 생략할수밖에 없었다 ㅠㅠ

이어서 정해진 프로젝트 컨셉을 바탕으로 요구사항 분석 및 IA를 진행하였다. 이전에 기획 경험이 있던 승연님을 필두로 아래와 같이 기능들을 금방 정리해나갔다.

image

이후, 요구사항을 바탕으로 승연님이 와이어프레임을 구상해주셨다. 확실히 피그마를 다루는 능력이 대단하셨다. 슥삭슥삭 페이지를 그리시더니 순식간에 10개가 넘는 페이지에 대한 와이어프레임이 완성되었다. 마치 2배속 영상을 보는것처럼 속으로 감탄하면서 우리팀에 이런 장인이 있어서 너무 좋다는 생각을 했다.

마지막으로, 역할 분배를 진행했다. 나는 항상 이 단계가 어렵다고 생각이 든다. 특히 이번 주제는 socket이라는 새로운 기술 스택을 사용해야 하는 프로젝트이기때문에 모두에게 socket을 다뤄볼 기회가 필요했고, 단계별로 이전 기능이 구현되지 않으면 이후 기능을 구현하지 못하는 구조였기 때문에 더욱 어려웠다. 결국 agile 방법론을 적용하여 3단계로 나누어 기능구현을 하기로 했다.

첫번째 단계에는 회원가입/로그인, 퀴즈, 기숙사 채팅방 생성, 헤더 및 디자인 파트가 있었는데 경규님은 회원가입/로그인을 희망하셨고, 승연님은 퀴즈, 문용이는 헤더 및 디자인을 해보고싶다고 해서 승준님과 내가 기숙사 채팅방 생성 기능을 맡게 되었다.

오프라인 미팅

다음날인 화요일에는 오프라인 미팅을 했다. 승연님과 문용이는 ui 디자인 고도화를 먼저 진행하셨다. 이날도 디자인을 맡아준 두명의 실력에 엄청나게 감탄할 수 밖에 없었다. 해리포터라는 컨셉이라 에셋을 구하기도 힘들고 디자인적으로 고려해야할 사항이 많이 쉽지 않을 것 같았지만 하루 사이에 아래와 같이 완성도 있는 UI를 뽑아주셨다… ㄷㄷ

image

그 사이 승준님과 나는 socket과 제공된 API에 대하여 공부를 했다. 나는 간단한 소캣 채팅을 만들어보면서 공부했다.

또 마침 nextjs 마스터 민섭이가 강의장에 왔길래 next에 대해서 몇가지 질문을 하였다. ssr 구현을 위한 server component의 개념에 대해서 민섭이가 귀에 쏙쏙 박히게 설명해주었다.

채팅방 생성

그 다음날인 수요일에는 같은 역할을 맡은 승준님과 강의장에서 기숙사 채팅방을 생성하는 기능을 같이 구현해보는 시간을 가졌다. 처음에 페어프로그래밍을 제안드렸지만 별로 안좋아하시는 눈치셔서 각자 구현해보기로 하였다.

어제 승연님과 문용이가 만들어준 메인 UI를 먼저 구현하였다. 기숙사 로고를 클릭하면 해당 기숙사 페이지로 이동하고 그 안에서 채팅방을 생성하는 간단한 로직을 생각했다.

useEffect(() => {
  axios
    .post(CREATE_CHAT_URL, requestData, { headers })
    .then((response) => {
      console.log('Request was successful:', response.data);
      setDormId(response.data.id);
    })
    .catch((error) => {
      console.error('Error sending the request:', error);
    });
}, []);

useEffect(() => {
  const socket = io(`https://fastcampus-chat.net/chat?chatId=${dormId}`, {
    extraHeaders: headers,
  });

  socket.on('new-chat', (res) => {
    console.log('채팅방이 생성되었습니다', res);
  });
}, [dormId]);

useEffect hook을 이용하여 axios 요청으로 채팅방을 생성하고, response로 받은 id값을 state로 설정해주고, 두번째 useEffecthook에서 dormId의 값에 변경이 있으면 해당 id로 socket을 여는 로직이었다.

복잡하지도 않아서 금방 기능구현을 완료할 줄 알았지만, 아무리 해도 채팅방이 생성되었다는 로그가 찍히지 않았다. 지금 와서 보니까 new-chat event는 server 연결시 사용할 수 있는 event중 하나인데, 기본 연결에 해당하는 url로 소캣을 열고 new-chat event를 사용하려고 하니 당연히 안찍혔던 것이 맞았다. 그것도 모르고 한참을 고민했던 기억이 난다 ㅠㅠ.

또, 위에서 말한 문제를 해결한답시고 의존성배열에 이것저것 넣어보다가 채팅방이 800개 넘개 생성되어서 멘토님한테 DM을 보낸 기억도 난다. 너무 당황해서 앞으로 주의하겠다고 말씀드렸는데, 멘토님이 그런 시행착오를 겪는 것이 당연하다고 해주셔서 감동이었다… 😢

어찌저찌 채팅방을 생성하고, 이제 간단하게 채팅을 치고 채팅 목록을 불러오는 기능을 구현해보기로 했다. 사실 어제 저녁먹으면서 승준님이랑 “금방 끝나겠는데요?” 하고 1단계 기능구현 due date인 목요일 전에 채팅 기능까지 먼저 개발해보자는 얘기까지 나와서 자신만만해 있었다.

하지만, socket은 분명히 잘 열렸고, 메세지 전송도 잘 되는 것을 확인했지만 아무리 해도 대화목록을 불러오는 것이 안됐다. 분명히 socket 이벤트 설명은 아래와 같이 되어있는데 말이다…

messages-to-client

- 이전 대화 목록을 불러옵니다.

응답 데이터
```ts
interface Message {
  id: string;
  text: string;
  userId: string; // 메세지를 보낸 사람의 id
  createdAt: Date;
}

interface ResponseData {
  messages: Message[];
}

승준님과 몇시간동안 씨름하다가 결국 찾아낸 답은 on event를 보내기 전에 emit event로 fetch를 해줘야한다는 것이었다…

fetch-messages

- 이전 대화 목록을 불러옵니다.
- messages-to-client로 데이터를 받을 수 있습니다.

fetch-messages event의 설명이 messages-to-client와 정확히 같아서 둘중에 하나만 사용해도 된다고 생각했고, 대화 목록은 server에서 client로 전송되는게 당연하다고 생각하여 on event인 messages-to-client만 사용했기에 메세지를 불러올수가 없었던 것이었다.

결국 승준님과 git에 비유해서 이해하기로 했다. git pull 이전에 git fetch를 해주어야 하는 것처럼, messages-to-client라는 on event가 일어나기 전에 fetch-messages로 먼저 emit event를 발생시켜주어야 한다고 생각하니 이해가 쉬웠다.

socket에 대한 공부가 부족했던 탓도 있었지만, socket을 처음 접하는 우리에게 조금 더 상세하고 친절한 설명이 있었으면 좋겠다는 생각이 들었다 ㅠㅠ

1차 멘토링

결국 목요일 전에 채팅 기능까지 먼저 개발해보자는 야망을 뒤로하고 우리도 due date에 맞춰서 기능구현을 완료했다. 다른 분들도 얼추 1단계 기능구현이 완료되어서 그동안 궁금했던 것을 멘토링때 물어보기로 했다. 멘토링때 질문했던 것들중 핵심적인 것은 크게 두 가지 정도가 있었다.

먼저, 기존에 제공되는 API가 담을 수 있는 값 외에 필요한 정보가 더 많을 때, 기존 값에 구분자를 추가하여 별도의 정보를 담아도 되냐는 질문이었다. (ex) 그리핀도르-UserName, 그리핀도르-ChatName)

멘토님의 답변은 기본적으로 제공된 API를 사용하는 것이 맞지만, 채팅방에 대한 column이 부족하다면, id를 기준으로 firebase에 똑같은 컬렉션, 똑같은 객체를 생성한 이후 필요한 데이터를 추가하라고 하셨다. 아니면, 질문대로 구분자로 구분하여 프론트단에서 split 후 필요한 데이터를 사용하는 방식을 사용해도 된다고 하셨다.

내 생각에는 둘 다 비효율적인 방법이라고 생각되었다. 멘토님도 실제로 비효율적인 방법이 맞지만, 지금처럼 API의 변경이 이루어지지 않을 때 사용할 수 있는 제한적인 방법이라고 하셨다.

결국 우리는 모든 채팅방(기숙사, 클럽)을 1-depth로 구현하기로 하였고, user에 대해서는 기숙사 정보만 firebase에 저장하기로 하였다.

두번째로, 이후의 역할 분배였다. 기존에 3단계로 구성된 계획은 이전 작업이 완료되지 않으면 이후 작업이 이어질 수 없기 때문에 멘토님은 차라리 소켓 이외 작업을 다 구현해놓고 동일 선상에서 각자 소켓 기능을 구현해보는 것을 추천해주셨다.

다음날 회의에서 이 방법을 적용할지 고민을 해보았지만, 아직 경규님 로그인/회원가입 작업이 끝나지 않은 상태였고, 문용이도 작업이 남아있었다. 승연님도 클럽 채팅방 부분을 구현해주신다고 해서 승준님과 내가 채팅 기능을 나누어 작업하기로 하였다.

기능 구현

이번 섹션에서는 내가 구현했던 기능 중에 기억에 남는 기능 몇개를 소개해보겠다.

접속 상태인 user 구분

useEffect(() => {
  const fetchData = async () => {
    try {
      const allUsersFromDB = await getFirebaseDatabyKeyVal('users');
      const tmpArr = participants.map((userRes) => {
        const matchingData = allUsersFromDB.find(
          (userDB) => userDB.id === userRes.id
        );

        return matchingData
          ? { ...userRes, class: matchingData.class }
          : userRes;
      });

      setUsers(tmpArr);
    } catch (error) {
      console.error('Error fetching data:', error);
    }
  };

  fetchData();
}, []);

...

const getStatusCircleColor = (participant: string): boolean => {
  return isConnected?.includes(participant);
};

...

return (
  ...

  {users?.map((user, index) => (
    <UserItem key={index}>
    <ProfileImage
      src={user.picture}
      alt="Profile"
    />
    <UserInfo>
      <Username>
        {user.username}{' '}
        {getStatusCircleColor(user.username) ? (
          <Emoji>🟢</Emoji>
        ) : (
          <Emoji>🔴</Emoji>
        )}
      </Username>
      <UserDormitory>{user.class}</UserDormitory>
    </UserInfo>
    </UserItem>
  ))}

  ...
}

위 코드는 접속 상태인 user를 구분하는 기능이다. 먼저, 첫번째 useEffecthook은 현재 채팅방 참가자들의 기숙사 정보를 설정해주는 hook이다. allUsersFromDB는 db에 저장된 모든 user의 데이터이다. participants는 props로 받은 현재 채팅방 참가자의 데이터이다. 먼저, allUsersFromDB에서 받아온 id값과, participants에서 받아온 id값을 비교하여 같은 것이 있는지 찾는다. 이후, 같은 것이 있으면, 기존 user 정보에 db에만 존재하는 class(기숙사) 정보를 넣어서 return해주고 그렇지 않으면 기존 user 정보를 그대로 return 하여 user라는 객체 배열 상태로 설정한다.

getStatusCircleColor함수는 user의 name을 parameter로 받아 props로 받은 접속 상태인 user들의 이름이 담긴 배열인 isConnect에 해당 user의 이름이 존재하는지의 여부를 return하게 된다.

렌더링 시, 위에서 설정해둔 user 배열을 순회하면서 각 user에 대한 정보와 접속 상태를 emoji의 형태로 출력한다.

이러한 로직은 조금씩 다르지만, 초대하기 모달, 채팅방 정보 모달, 전체 user 목록 등에서 사용되었다. 리팩토링 시 모듈화해서 편리하게 사용할 수 있도록 만들어보고싶은 기능중 하나이다.

image

image

현재 채팅에 없는 사람만 초대

// 현재 채팅에 있는 user 불러오기
  useEffect(() => {
    axios
      .get(GET_ALL_CHATTINGS_URL, { headers })
      .then((response) => {
        const chats = response.data.chats;

        const { users } = chats?.find((room) => room.id === chatId);
        setCurrentChatUsers(users);
      })
      .catch((error) => {
        console.error('현재 채팅에 있는 user 불러오기 실패!', error);
      });
  }, []);

  // 초대 가능한 user 불러오기(현재 채팅에 없는 사람만)
  useEffect(() => {
    axios
      .get(GET_ALL_USERS_URL, { headers })
      .then(async (response) => {
        console.log('모든 user: ', response.data);
        console.log('현재 채팅에 있는 user: ', currentChatUsers);

        const otherUsers = response.data.filter(
          (user) =>
            !currentChatUsers.some(
              (currentChatUser) => user.id === currentChatUser.id,
            ),
        );

        ...
  }, [currentChatUsers]);

위 코드는 현재 채팅에 없는 사람만 초대하는 기능이다.

첫번째 useEffect hook을 통해 모든 채팅 정보를 불러온다. 이후, props로 받은 현재 채팅방의 chatId와 일치하는 room을 찾고, 구조분해할당을 통해 현재 채팅의 user 정보를 설정해준다.

두번째 useEffect는 초대 가능한 user, 즉 현재 채팅에 없는 사람만 불러오는 hook이다. 먼저, axios 요청을 통해 모든 user 정보를 불러온다. 이후, 배열 안에 어떤 요소라도 특정 조건을 충족하는지 확인하는 함수인 some()함수를 통해 currentChatUsers 배열에 포함된 어떤 user와도 일치하지 않는 user(현재 채팅방에 없는 user들)들를 찾아 filtering한다.

return (
  ...

  {
    allUsers
      .filter(
        (user) => !invitedUsers.some((invitedUser) => invitedUser.id === user.id)
      )
      .map((user, index) => (
        <UserItem key={index}>
          <ProfileImage src={user.picture} alt="Profile" />
          <UserInfo>
            <Username>{user.name}</Username>
            <UserDormitory>{user.class}</UserDormitory>
          </UserInfo>
          <styled.InviteIcon
            onClick={() =>
              setInvitedUsers((prevInvitedUsers) => [
                ...prevInvitedUsers,
                { id: user.id, name: user.name },
              ])
            }
          />
        </UserItem>
      ));
  }

  ...
)

이후, 렌더링 시 동일한 로직으로, invitedUsers 배열에 포함된 어떤 user와도 일치하지 않는 user들의 정보(초대하지 않은 user들의 정보)만 렌더링한다. 초대 버튼을 누르면, inviteUsers배열에 클릭한 user의 정보를 추가한다.

{
  invitedUsers.map((invitedUser, index) => (
    <styled.UserInfo key={index}>
      <Username>{invitedUser.name}</Username>
      <styled.CancelButton
        onClick={() => {
          const updatedInvitedUsers = invitedUsers.filter(
            (user) => user.id !== invitedUser.id
          );
          setInvitedUsers(updatedInvitedUsers);
        }}
      >
        X
      </styled.CancelButton>
    </styled.UserInfo>
  ));
}

invitedUser들은 모달 상단에 별도로 렌더링되는데, 특정 user에 대한 취소 버튼을 누를 시, filter()함수를 통해 invitedUser 배열을 update하고, 해당 user는 다시 초대하지 않은 user들의 정보가 렌더링되는 section에 표시되게 된다.

image

에러 핸들링

이번 섹션에서는 프로젝트를 진행하면서 겪었던 에러 내용들을 정리해보겠다.

먼저 무한 호출 에러이다.

export const useMessagesToClient = (chatSocket, setPreviousMessages) => {
  useEffect(() => {
    try {
      chatSocket.on('messages-to-client', (messageObject: any) => {
        setPreviousMessages(messageObject.messages);
        console.log('S->C 이전 대화목록 가져오기 성공!');
        return () => {
          chatSocket.disconnect();
        };
      });
    } catch (error) {
      console.error('S->C 이전 대화목록 가져오기 실패!', error);
    }
  }, [chatSocket, setPreviousMessages]);
};

처음에는 위와 같은 형태로 승준님이 hook들을 모듈화해서 구성해 주셨다. socket 객체를 parameter로 받아 event를 발생시키고, unmount시 socket을 disconnect해주었다. 의존성 배열에는 socket을 넣어주어 socket 객체에 변화가 있을 때만 useEffect hook이 실행되는 로직이었다.

호출은 채팅방 페이지의 선언부에서 필요한 hook들을 차례로 호출해주었다.

위 방법대로 동작을 시켰을 때, unmount시 socket을 disconnect 시켜주었음에도 불구하고 대화목록이 무한으로 가져와지는 에러가 있었다. 로직상 문제가 없다고 생각되었는데 왜 계속 호출이 일어나지 하고 socket 객체를 console로 찍어보았다. 무슨 이유인지는 모르지만 id값이 다른 socket들이 계속 생성되고 있었다.

socket은 한 번 열리면 다시 열기 전까지 재생성되지 않는 줄만 알고있었는데 console에 찍어보니 그게 아니었던 것이다. 마침 강의장에 저번에 비슷한 고민을 가지고 계시던 지수님과 지민님이 계시길래 질문을 드려보았다.

지수님과 지민님도 이전에 무한호출 문제를 겪으셔서 멘토링 시간에 관련해서 질문을 드렸었다고 한다. 두분이 제안해주신 방법은 socket생성 시 useMemo로 감싸서 caching을 해보라는 것이었다. 생각지도 못한 방법이었다.

const chatSocket = useMemo(() => {
  return io(`https://fastcampus-chat.net/chat?chatId=${chatId}`, {
    extraHeaders: headers,
  });
}, [accessToken]);

그래서 위처럼 useMemo를 통해 이미 만들어진 socket이 있으면 caching을 해서 불필요한 재생성을 막아주었다. 결과는 대성공이었고 한참 고민했던 문제가 해결된 나머지 너무 기뻐서 감사해서 두분께 초코에몽을 사드렸다 ㅎㅎ… 처음 뵙는 분들이었는데 친절하게 솔루션을 공유해주셔서 다시 한 번 감사드린다는 말씀 전하고 싶다.

두번째는 채팅방 페이지 접속 시 채팅이 안보이는 에러이다. 승준님이 이 에러 관련 핸들링을 나에게 맡기셔서 에러 핸들링 과정을 정리해보도록 하겠다.

useFetchMessages(chatSocket);
useMessageToClient(chatSocket);
useMessagesToClient(chatSocket, setPreviousMessages);
useFetchUsers(chatSocket);
usePullUsers(chatSocket, setIsConnected);
useJoinUsers(chatSocket, setIsConnected);
useLeaveUsers(chatSocket, setIsConnected);

처음에 승준님이 구성하신 로직에서는 위에서도 말했듯이 페이지 선언부에서 socket 관련 hook들을 호출해주었다.

나는 약간 방식을 바꾸어서 hook들을 단순히 함수 형태로 아래와 같이 변환하였다.

export const useMessagesToClient = async (
  chatSocket,
  handleMessagesToClient
) => {
  try {
    await chatSocket.on('messages-to-client', handleMessagesToClient);
  } catch (error) {
    console.error('S->C 이전 대화목록 가져오기 실패!', error);
  }
};

그리고, parameter로 handler 함수를 통째로 전달해 주었다.

const handleMessagesToClient = (messageObject) => {
  setPreviousMessages(messageObject.messages);
  console.log('S->C 이전 대화목록 가져오기 성공!');
};

이후, 호출 시 아래와 같이 useEffect 훅으로 socket관련 함수들을 한번에 호출해주었다.

useEffect(() => {
  useFetchMessages(chatSocket);
  useMessageToClient(chatSocket, handleMessageToClient);
  useFetchUsers(chatSocket);
  usePullUsers(chatSocket, handlePullUsers);
  useJoinUsers(chatSocket, handleJoinUsers);
  useLeaveUsers(chatSocket, handleLeaveUsers);

  return () => {
    console.log('Cleanup function called');
    chatSocket.off('message-to-client', handleMessageToClient);
    chatSocket.off('users-to-client', handlePullUsers);
    chatSocket.off('join', handleJoinUsers);
    chatSocket.off('leave', handleLeaveUsers);
  };
}, [chatSocket]);

useEffect(() => {
  useMessagesToClient(chatSocket, handleMessagesToClient);

  return () => {
    chatSocket.off('messages-to-client', handleMessagesToClient);
  };
}, [chatSocket, setPreviousMessages]);

위 로직대로 선언부에서 단순 호출해주는것이 아니라, useEffect hook을 사용해서 렌더링 시 socket 관련 함수들을 모두 호출하고, unmount시 해당 event를 off해주도록 수정해서 1차적으로 해결을 한 줄 알았으나… 다시 채팅이 안보이는 문제가 있다는 제보가 들어왔다.

  ...

  useEffect(() => {
    const fetchDataAndAttachListeners = async () => {
      try {
        await useFetchMessages(chatSocket);
        await useFetchUsers(chatSocket);
        useMessageToClient(chatSocket, handleMessageToClient);
        usePullUsers(chatSocket, handlePullUsers);
        useJoinUsers(chatSocket, handleJoinUsers);
        useLeaveUsers(chatSocket, handleLeaveUsers);
      } catch (error) {
        console.error(
          '데이터 가져오기 또는 이벤트 리스너 추가 중 오류 발생:',
          error,
        );
      }
    };

    fetchDataAndAttachListeners();

    return () => {
      console.log('클린업 함수 호출됨');
      chatSocket.off('message-to-client', handleMessageToClient);
      chatSocket.off('users-to-client', handlePullUsers);
      chatSocket.off('join', handleJoinUsers);
      chatSocket.off('leave', handleLeaveUsers);
    };
  }, [chatSocket]);

  ...

그래서 해결방법을 고민하다가 fetch 함수들에 대한 비동기 처리를 해주었다. on event들이 발생하기 전에 emit event로 client에서 server측으로 fetch 요청을 보내야하기 때문에, 해당 함수들이 실행 되고 on event가 발생할 수 있도록 비동기 처리를 해주었다.

결과적으로 다행히 내 솔루션이 잘 먹힌것 같았다. 채팅방에 입장하자마자 채팅이 바로바로 불러와졌다.

사실 이 에러 핸들링은 소켓에 대한 이해가 충분하지 않았기 때문에 발생했던 것 같다. 아직도 왜 id가 다른 socket이 계속 생성되었는지에 대한 이유를 정확히 파악하지 못했다. 나중에 기회가 된다면, socket이 내부적으로 어떻게 동작하는지 세세하게 공부해보고 싶다.

마치며…

2주가 금새 흘러갔고, 어찌저찌 프로젝트가 잘 마무리되었다. 사실 배포 전날 1차 qa를 하면서 제대로 동작하는 기능이 거의 없어서 많이 걱정했다.

image
(처참했던 qa 결과물…)

그래도 집에 가면서 내 한탄(?)을 들어주고 잘 될거라고 말해준 문용이 덕분인지 비교적 금방 에러를 잡고 필수 기능 구현은 무사히 완료된 것 같다.

처음에 기획한 마법 주문을 외우면 모션을 추가하거나 볼드모트를 퇴치하는 기능 구현은 고사하고 배포시 console log 출력과 favicon 설정도 못해주었지만 2주동안 각자 기능 구현 열심히 해주었던 우리 팀원들 ⭐승준님, 승연님, 문용이, 경규님⭐에게 고맙다는 말 전하고 싶다.

마지막으로 사이드 프로젝트와 이번 토이프로젝트2, 앞으로 미니프로젝트를 앞두고 있는 나에게도 스스로 고생했다고 다독여주고싶다. 주말에 푹 쉬고 다음주에 힘내서 미니프로젝트도 화이팅!! 💪💪

카테고리:

업데이트: