이제 리액트에서 채팅 서비스를 만들 모든 준비가 끝났다!
우선 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
에 넣어줘야 표시가 된다.
다행히 정상적으로 채팅 목록이 표시되는 것을 볼 수 있다. 아직 초기값으로 넣어준 채팅 하나밖에 없기 때문에 하나만 표시된다.
채팅 입력 컴포넌트
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
에 넣어줘야 표시가 된다.
지금까지는 채팅 작성은 적용 되었지만 새로고침을 하지 않으면 채팅이 입력 되었는지 입력되지 않았는지 알기 힘들다. 그렇기 때문에 이제 채팅 구독 기능을 적용해야 한다.
채팅 구독 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
로 받은 data
를 setState
하는 것 처럼 재설정 된다. 그렇기 때문에 새로 받아온 subscriptionData
를 원래 있던 getChatList
배열안에 넣어주면 채팅이 업데이트 될 때마다 data
도 업데이트 된다.
playground를 통해 채팅을 보내봤더니 정상적으로 채팅이 추가되는 것을 볼 수 있었다. 이것으로 Apollo Server와 리액트, 타입스크립트를 사용한 간단한 채팅 서비스 개발이 완료되었다!
후기
사실 GraphQL을 이전부터 써보긴 했지만 많이 알고있지 못했고 특히 subscription에 대한 내용은 거의 몰랐는데 이번에 프로젝트를 하며 실시간 통신 기능이 많이 필요했다. 그래서 한번 정리해 볼겸 채팅 서비스를 개발해 봤는데 생각보다 새로 알게된 내용이 많아서 좋았다.
항상 포스팅을 하며 새로 알게되는 내용이 많아지는 것 같아서 자주자주 포스팅 하도록 노력해야겠다.