thumnail.png

이전 포스트 : React와 GraphQL로 채팅 구현하기 - 리액트 환경설정

이제 리액트에서 채팅 서비스를 만들 모든 준비가 끝났다!

우선 server에서 정의한 GraphQL 쿼리를 클라이언트에서 사용하기 위해 해당 쿼리를 작성해야 한다. 방식은 이전 포스트에서 했던 테스트 요청 쿼리와 동일하게 작성하면 된다.

채팅 서비스 쿼리 작성

채팅 서비스 쿼리는 서버 API를 작성할 때 만든 3가지의 쿼리(채팅 작성, 채팅 목록 조회, 채팅 구독)만 작성하면 된다.

import { gql } from '@apollo/client';

// 채팅 목록 조회
export const GET_CHAT_LIST = gql`
  query GetChatList { # operation name
    getChatList {
      id
      writer
      content
    }
  }
`;

// 채팅 작성
export const ADD_CHAT = gql`
  mutation AddChat($writer: String!, $content: String!) {
    addChat(writer: $writer, content: $content) {
      result
      error
    }
  }
`;

// 채팅 구독
export const SUB_CHAT = gql`
  subscription SubChat {
    subChat {
      id
      writer
      content
    }
  }
`;

쿼리는 src/queries/chat.queries.ts파일에 작성하면 된다. 이때 파일 명엔 반드시 queries.ts를 확장자로 작성해야 타입 제너레이터가 타입을 생성해준다. (설정을 바꾼다면 다른 이름으로도 사용 가능하다)

서버에서 playground로 작성했을 때는 위의 operation name을 따로 작성하지 않아도 됐지만 타입 제너레이터가 저 operation name을 타입명으로 타입을 생성시켜주기 때문에 반드시 입력시켜줘야 한다.

채팅 목록 조회 컴포넌트

import React, { FC } from 'react';
import { useQuery } from '@apollo/react-hooks';

import { GET_CHAT_LIST } from '../queries/chat.queries';
import { GetChatList } from '../api';

const Home: FC = () => {
  const { loading, data } = useQuery<GetChatList>(GET_CHAT_LIST);

  return (
    <div>
      {loading ? (
        <h3>loading...</h3>
      ) : (
        data?.getChatList.map((chat) => (
          <div key={chat.id}>{`${chat.writer}: ${chat.content}`}</div>
        ))
      )}
    </div>
  );
};

export default Home;

src/components/ChatList.tsx에 다음과 같이 채팅 목록을 조회 후 표시해주는 컴포넌트를 만들었다. useQuery를 통해 아까 만든 GET_CHAT_LIST이라는 쿼리를 요청할 수 있다.

useQuery를 통해 쿼리를 호출하면 자동으로 loading, data의 상태를 넘겨주기 때문에 loading이 종료되고 data가 있을 때만 채팅 리스트를 map을 통해 표시해줬다.

import React, { FC } from 'react';
import { ApolloProvider } from '@apollo/react-hooks';

import apollo from './apollo';
import ChatList from './components/ChatList';

const App: FC = () => {
  return (
    <ApolloProvider client={apollo}>
      <ChatList />
    </ApolloProvider>
  );
};

export default App;

이렇게 만든 컴포넌트는 src/app.tsx에 넣어줘야 표시가 된다.

1.png

다행히 정상적으로 채팅 목록이 표시되는 것을 볼 수 있다. 아직 초기값으로 넣어준 채팅 하나밖에 없기 때문에 하나만 표시된다.

채팅 입력 컴포넌트

import React, { FC, useCallback, useState } from 'react';
import { useMutation } from '@apollo/react-hooks';

import { ADD_CHAT } from '../queries/chat.queries';
import { AddChat } from '../api';

const Input: FC = () => {
  const [writer, setWriter] = useState('');
  const [content, setContent] = useState('');
  const [addChatMutation] = useMutation<AddChat>(ADD_CHAT, {
    variables: { writer, content },
  });

  const onChangeWriter = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setWriter(e.target.value);
    },
    []
  );
  const onChangeContent = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setContent(e.target.value);
    },
    []
  );

  const onSubmit = useCallback((e: React.FormEvent) => {
    e.preventDefault();
    addChatMutation();
  }, []);

  return (
    <form onSubmit={onSubmit}>
      <input
        type="text"
        value={writer}
        onChange={onChangeWriter}
        placeholder="작성자"
      />
      <input
        type="text"
        value={content}
        onChange={onChangeContent}
        placeholder="내용"
      />
      <button>입력</button>
    </form>
  );
};

export default Input;

src/components/InputChat.tsx에 채팅 입력 컴포넌트를 새로 만들어줬다. useMutation에 우리가 만든 채팅 작성 쿼리를 넣어주면 해당 쿼리를 실행할 수 있는 mutation함수를 배열에 넣어서 반환해준다.

작성자와 내용을 입력받아 addChatMutation을 실행하면 서버로 해당 api를 요청한다. (초기에 mutation을 선언할 때 variables에 작성자와 내용에 대한 데이터를 넣어줬다)

import React, { FC } from 'react';
import { ApolloProvider } from '@apollo/react-hooks';

import apollo from './apollo';
import ChatList from './components/ChatList';
import InputChar from './components/InputChat';

const App: FC = () => {
  return (
    <ApolloProvider client={apollo}>
      <ChatList />
      <InputChar />
    </ApolloProvider>
  );
};

export default App;

InputChat 컴포넌트 역시 src/app.tsx에 넣어줘야 표시가 된다.

2.png

지금까지는 채팅 작성은 적용 되었지만 새로고침을 하지 않으면 채팅이 입력 되었는지 입력되지 않았는지 알기 힘들다. 그렇기 때문에 이제 채팅 구독 기능을 적용해야 한다.

채팅 구독 Subscription 적용

import React, { FC, useEffect } from 'react';
import { useQuery } from '@apollo/react-hooks';

import { GET_CHAT_LIST, SUB_CHAT } from '../queries/chat.queries';
import { GetChatList, SubChat_subChat as Chat } from '../api';

interface SubChats {
  subscriptionData: { data: { subChat: Chat } };
}

const Home: FC = () => {
  const { loading, data, subscribeToMore } = useQuery<GetChatList>(
    GET_CHAT_LIST
  );

  // 실시간 채팅 구독 설정
  useEffect(() => {
    subscribeToMore({
      document: SUB_CHAT,
      updateQuery: (prev, { subscriptionData }: SubChats) => {
        if (!subscriptionData.data) return prev;
        const newChat = subscriptionData.data.subChat;
        return { getChatList: [...prev.getChatList, newChat] };
      },
    });
  }, []);

  return (
    <div>
      {loading ? (
        <h3>loading...</h3>
      ) : (
        data?.getChatList.map((chat) => (
          <div key={chat.id}>{`${chat.writer}: ${chat.content}`}</div>
        ))
      )}
    </div>
  );
};

export default Home;

src/components/ChatList.tsx에 실시간 채팅 기능을 적용했다. useQuery에서 가져온 subscribeToMore함수는 쿼리에 구독 기능을 넣어서 해당 구독에 데이터가 업데이트 될 때마다 감지해 쿼리를 업데이트 해준다. apollo에서 제공하는 useSubscription이라는 hooks도 있지만 채팅 목록이 추가될 때마다 원래 사용하던 쿼리 데이터를 업데이트 하는 방식이 좀 더 좋을 것 같아 subscribeToMore를 사용했다.

해당 함수는 document프로퍼티에 구독 쿼리인 SUB_CHAT를 넣어주면 해당 subscription에 구독이 발생할 때마다 updateQuery에 넣어준 함수가 실행된다.

updateQuery에 넣어준 함수의 반환값은 useQuery로 받은 datasetState하는 것 처럼 재설정 된다. 그렇기 때문에 새로 받아온 subscriptionData를 원래 있던 getChatList 배열안에 넣어주면 채팅이 업데이트 될 때마다 data도 업데이트 된다.

chat.gif

playground를 통해 채팅을 보내봤더니 정상적으로 채팅이 추가되는 것을 볼 수 있었다. 이것으로 Apollo Server와 리액트, 타입스크립트를 사용한 간단한 채팅 서비스 개발이 완료되었다!

후기

사실 GraphQL을 이전부터 써보긴 했지만 많이 알고있지 못했고 특히 subscription에 대한 내용은 거의 몰랐는데 이번에 프로젝트를 하며 실시간 통신 기능이 많이 필요했다. 그래서 한번 정리해 볼겸 채팅 서비스를 개발해 봤는데 생각보다 새로 알게된 내용이 많아서 좋았다.

항상 포스팅을 하며 새로 알게되는 내용이 많아지는 것 같아서 자주자주 포스팅 하도록 노력해야겠다.