하투-시그널 개발기. ( + React로 채팅 구현하는 법)

김민성·2023년 12월 30일
10

회고

목록 보기
6/9
post-thumbnail

Frontend로써 개발을 시작한 후 3번째로 세상에 내놓은 "하투-시그널" 이라는 서비스이다.
동국대학교 가을 축제 기간을 즐길 사람들을 위해 준비했다. GDSC DGU 커뮤니티에 들어간 후, 처음으로 함께 기획하고 개발했던 경험이었다.

0. 서비스 소개

축제 기간 이성과의 만남을 목적으로 동국대학교의 축제를 즐기러 올 사람들을 위한 N:N 남녀 매칭 서비스이다.

친구의 닉네임을 추가해 그룹을 결성한 후, 그룹의 간단한 소개를 작성한 후 매칭을 기다린다. 이 때 매칭을 기다리는 리스트에 올라가게 된다. 서로에게 '시그널'을 날렸을 때, '맞시그널'로 판단되어 두 이성 그룹 간의 매칭은 성공하고 채팅방이 자동으로 개설되어 소통을 할 수 있다.

추가적으로, 주점의 위치와 간략한 정보를 알려주고, 주점 별 오픈채팅방 또한 제공한다.

1. 개발하게 된 이유

대학 축제를 찾는 사람들의 일부분의 목적에 초점을 두었다.
우리가 초점을 둔 목적은 "이성과의 만남" 이었다.
꽤 많은 사람들이 이성과의 만남을 목적으로 두고 대학 축제에 방문한다고 한다.

하지만, 직접 이성에게 다가가 선뜻 말을 거는 것이 어려운 일이다. 하지만, 이를 보완해 줄 플랫폼이 있다면 어떨까? 당당히 말할 수 있을 것이다.

"여기 주점 오른쪽 제일 끝 테이블에 여성 분 제 스타일이신데, 저희 테이블이랑 합석하시는 거 어떠세요?"

이런식으로 말이다. 또한, 이 마저도 어려울 사람들도 있을 것이다. 결국 이 기능이 메인 기능이 되었다.

혼자 또는 함께 찾아온 친구들과 그룹을 맺게 해서 인원 수에 맞는 이성 그룹을 매칭해 함께 소통하고, 더 나아가 함께 대면으로 만나서 즐기게 할 수는 없을까?

1. 혼자 또는 친구의 닉네임을 추가해 그룹을 결성한다.
2. 그룹의 간단한 소개를 작성한 후 매칭을 기다린다.
3. 이 때 매칭을 기다리는 리스트에 올라가게 된다.
4. 서로에게 '시그널'을 보냈을 때, '맞시그널'로 판단되어 두 이성 그룹 간의 매칭은 성공한다.
5. 채팅방이 자동으로 개설되어 채팅을 할 수 있게 된다.

2. 채팅.. 어떻게 구현해야하지..?

내가 맡은 부분이 마이페이지, 신고페이지, 주점 별 채팅방, 매칭 후 채팅방 이 4가지였다.
사실 앞의 두 페이지는 크게 어려울 것이 없었고, 뒤의 두 페이지가 나에겐 큰 시련(?)이었다.

물론 내가 직접 나서서 "제가 채팅 맡아보겠습니다!" 라고 당당히 말했지만.. 한 번도 안해본 부분이었다.
그래도 항상 프로젝트를 하게 되면 사용해보지 않은 새로운 기술을 적용해보고자 노력해왔고, 이는 나의 성장을 촉진할 수 있을 것이라는 생각에 맡게 되었던 것 같다. (멘땅에 헤딩하기!)

3. 채팅 구현하기

우선 채팅 데이터를 다음과 같이 관리해준다.

interface Message {
  sender: string;
  content: string;
  sendTime: string;
}
  const [messages, setMessages] = useState<Message[]>([]);

connectToServer()

1. 중복 연결 확인

WebSocket 연결이 이미 활성화되어 있는지 확인한다.
이미 연결된 경우, 추가 연결 시도를 방지하기 위해 함수 실행을 중단한다.

if (stompClientRef.current && stompClientRef.current.connected)

2. WebSocket 연결 설정

WebSocket 서버의 URL을 구성한다.

const fullUrl = ${import.meta.env.VITE_APP_SERVER_HOST}/ws-connection;

SockJS 클라이언트를 생성하여 서버의 WebSocket endpoint와 연결한다.

const socket = new SockJS(fullUrl);

생성된 SockJS 소켓을 Stomp 프로토콜로 감싸 WebSocket 통신을 가능하게 한다.

stompClientRef.current = Stomp.over(socket);: 생성된 SockJS 소켓을 Stomp 프로토콜로 감싸 WebSocket 통신을 가능하게 합니다.

3. Stomp 클라이언트 설정

Stomp 클라이언트를 초기화한다. 여기서 webSocketFactory는 WebSocket 연결을 생성하는 팩토리 함수이다.

const client = new Client({webSocketFactory: () => socket});: 

4. 연결 이벤트 핸들링

WebSocket 연결이 성공했을 때 실행되는 콜백이다.
이 콜백 내부에서 /subscribe/rooms/${chatId} 경로를 구독하여 해당 채팅방의 메시지를 수신할 수 있게 설정한다.
수신된 메시지는 showMessageOutput 함수를 통해 처리된다.

client.onConnect = (frame) => {...} 

WebSocket 연결 중 오류가 발생했을 때 실행되는 콜백이다.
오류가 발생하면 콘솔에 오류를 출력하고, 5초 후에 connectToServer 함수를 재실행하여 연결을 재시도한다.

client.onStompError = (error) => {...}: 

5. Stomp 클라이언트 활성화

설정된 Stomp 클라이언트를 활성화한다.

client.activate();

참조 변수에 활성화된 클라이언트를 할당한다.

stompClientRef.current = client;

sendMessage()

1. WebSocket 연결 상태 확인

WebSocket 연결이 활성화되어 있는지 확인한다.
연결이 활성화되지 않았다면, 오류 메시지를 출력하고 함수 실행을 중단한다. 이는 메시지 전송을 시도하기 전에 유효한 연결 상태를 보장하기 위함이다.

if (!stompClientRef.current || !stompClientRef.current.connected)

2. 메시지 객체 생성

메시지를 전송하기 위한 객체를 생성한다. 이 객체는 세 가지 주요 속성을 포함한다.

  • sender: 메시지를 보내는 사용자의 닉네임.
  • sendTime: 메시지가 전송되는 시간입니다. ISO 형식의 문자열로, new Date().toISOString()을 사용하여 현재 시간을 가져옴.
  • content: 사용자가 입력한 메시지 내용.
const chatRequest = {
  sender: nickname,
  sendTime: new Date().toISOString(),
  content: messageContent, // 사용자가 입력한 메시지 내용을 사용
};

3. 메시지 전송

Stomp 클라이언트를 사용하여 메시지를 서버에 전송한다.

  • destination: 메시지를 전송할 서버의 엔드포인트. 여기서는 /app/messages/${chatId} 형식을 사용하여 특정 채팅방의 엔드포인트로 메시지를 보냄.
  • body: 전송할 메시지의 내용. JSON.stringify(chatRequest)를 사용하여 chatRequest 객체를 JSON 형식의 문자열로 변환.
stompClientRef.current.publish({
  destination: `/app/messages/${barID}`,
  body: JSON.stringify(chatRequest),
});

showMessageOutput()

1. 수신된 메시지 객체 생성

수신된 메시지의 데이터를 사용하여 새로운 Message 객체를 생성한다.
이 객체는 메시지의 발신자(sender), 내용(content), 그리고 전송 시간(sendTime)을 포함한다.

const newMessage: Message = {
  sender: messageOutput.sender,
  content: messageOutput.content,
  sendTime: messageOutput.sendTime,
};

2. 메시지 목록 업데이트

현재 메시지 목록(prevMessages)에 새로운 메시지(newMessage)를 추가한다.
이는 React의 상태 업데이트 패턴을 사용하여 이전 상태에 기반해 새로운 상태를 생성한다.

setMessages((prevMessages) => [...prevMessages, newMessage])

메세지 보여주기

map함수를 이용해 랜더링 해준다. 메세지의 sender가 자신의 nickname과 일치할 땐, 화면의 오른쪽에 표시해주어야 하므로 삼항연산자를 이용해 분기처리를 해준다.

{messages &&
          messages.map((item, index) =>
            item.sender === nickname ? (
  ...

4. 유저의 요구에 의한 서비스 중 긴급 수정

주점 별 오픈채팅에서 채팅을 관리함에 있어, 다음과 같이 구현했었다.

유저가 1번 주점 채팅방을 나갔다가 재입장 -> 이전 채팅 내역이 존재
1번 주점 채팅방을 나가고 2번 주점 채팅방으로 입장 -> 1번 주점 채팅방에서의 채팅 내역 리셋

이 때 당시에 백엔드 단에서 데이터 관리의 어려움을 겪자 프론트 단에서 해결할 수 있는 방법은 채팅방 입장 시간, 나간 시간, 이전 채팅방 id, 현재 채팅방 id 이 4가지 값을 localStorage로 관리하는 것이었다. 이는 분명 큰 문제점을 가지고 있었지만, 출시가 얼마 남지 않은 상황에서의 최선의 방법이라 생각했다.

  useEffect(() => {
   //const beforePubID = localStorage.getItem("nowPubID"); // url을 직접 치고 들어갔을 때를 대비하기 위한 코드..
    connectToServer();

    // 현재 주점 이름을 localStorage의 nowPubID에 저장
    localStorage.setItem("nowPubID", barID ? barID : ""); // url을 직접 치고 들어갔을 때를 대비하기 위한 코드..

   // const nowPubID = localStorage.getItem("nowPubID");

    //if (nowPubID == beforePubID) {
      //같은 주점채팅방에 입장할 때
      //enterTime을 갱신하면 안됨. 왜냐하면 직전에 방문했던 주점채팅방에 다시 입장하는 것이기 때문에 주점 채팅방을 나간것이 아니기 때문.
      //const enterTime = localStorage.getItem("enterTime");

하지만 서비스를 운영하던 당일, 채팅방을 왔다갔다 하니 이전 채팅내역이 모두 사라져있는 것이 오히려 유저의 입장에서는 굉장한 불편을 느낀다는 제보가 들어왔다. 빠른 회의 결과 우리가 잘못 생각하고 개발했음을 인정하고 빠르게 HOTFIX를 진행해주었다. (그래서 저 부분들이 주석처리가 되어있는 것이다. ㅎㅎ..)

5. 마무리하며.


처음으로 많은 팀원과 함께하는 프로젝트였다. 4-5일간 개발했고, 많은 팀원이 함께 해 적절히 컴포넌트를 분리한다던지, 컨벤션을 맞추는 데에 어려움이 있었지만 이러한 부분에 있어서 삐걱댔음에도 결국엔 하투-시그널을 세상에 선보일 수 있었고, 해보지 못한 채팅을 구현할 수 있었다는 점, 정말 많은 사람과 프로젝트를 경험했다는 점에서 뜻깊은 경험이었다고 생각한다.

2일 간 운영하면서 약 600명 가량의 유저를 확보할 수 있었다. 가을 축제라 규모가 작았던 탓인지, 아니면 서비스를 이용함에 있어 불편한 점이 더 있었을지는 확답을 내릴 수는 없겠지만 다음 해 봄 축제를 할 때 부족했던 부분을 보완해서 재배포를 할 예정이다.

대학생때 하고 싶은 개발을 통해 얻을 수 있는 다양한 경험을 모두 해보고 싶다. 프로젝트를 할 때 항상 기존에 사용해보지 않은 기술을 적용하고자 노력할 것이다.

profile
다양한 활동을 통해 인사이트를 얻는 것을 즐깁니다. 저 또한 인사이트를 주는 사람이 되고자 합니다.

0개의 댓글