분류 Reactjs

Node.js, GraphQL 및 React를 사용하여 실시간 채팅 애플리케이션을 구축하는 방법 — 2

컨텐츠 정보

  • 조회 479 (작성일 )

본문

우리는 무엇을 만들고 있습니까?


https://medium.com/the-andela-way/how-to-build-a-real-time-chat-application-with-node-js-graphql-and-react-2-80ae53849e9b 


소개 


이 튜토리얼의 1부에서 실시간 채팅 애플리케이션을 위한 GraphQL 서버를 구축하는 데 성공했습니다. 이제 React로 프론트 엔드를 구축하고 React Hooks를 사용할 것입니다. create-react-app를 사용하여 부트 스트랩하고 Apollo 클라이언트를 사용하여 GraphQL 서버를 React에 연결합니다.


우리는 사용자가 이메일과 사용자 이름으로 등록 할 수 있기를 바랍니다. 유효한 정보를 입력하면 새로운 사용자를 만들기 위해 서버로 데이터를 보냅니다. 그런 다음 사용자의 세부 정보를 브라우저의 로컬 저장소에 저장하여 세션을 유지할 수 있습니다. 로그인 한 모든 사용자는 다른 로그인 한 사용자를 보고 채팅 할 사용자를 선택할 수 있습니다.


이것은 React, Apollo 또는 React Hooks에 대한 소개가 아닙니다. 그 일을 잘하는 다른 많은 자원이 있습니다. 나는 당신이 이미 React 생태계에 약간 익숙하다고 가정합니다. 아직 프로그래밍 경험이 없다면 따라갈 수 있습니다.


react 후크는 반응 16.8에 도입되었습니다. 후크 1을 사용하기 전에 React 16.8 이전에 도입 된 실험 버전을 사용하려는 경우를 제외하고 이 버전의 React가 있는지 확인하십시오. 이전에 React를 사용했다면 Hooks를 좋아할 것입니다. 코드를 더 깨끗하고 이해하기 쉽게 만듭니다.


크기가 크기 때문에 여기에 모든 코드를 표시 할 수는 없습니다. 우리가 움직일 때 설명하려고 노력할 것이다. GitHub 요지에서 해당 기능이 어떤 기능을 하는지 언급하는 '코멘트'라인을 찾으십시오. GitHub에서 모든 코드를 얻을 수 있으므로 걱정하지 마십시오. 요점을 완전히 보려면 ​​여기로 건너 뜁니다.


이제 우리가 달성하고자 하는 것에 대한 그림을 얻었으니 작업을 시작하겠습니다. GitHub에서 저장소를 복제하고 스핀을 위해 꺼낼 수 있습니다.


https://github.com/LukasChiama/chat_app_client?source=post_page-----80ae53849e9b----------------------


종속성 설치 


create-react-app를 전역으로 설치 한 경우 루트 폴더 및 터미널로 이동하여 실행하십시오.


create-react-app client 


원하지 않는 모든 파일의 src/폴더를 지우십시오. index.js, App.js 및 index.css 파일만 남겨 두십시오. src/ 폴더에 있는 동안 터미널에서 이 명령을 실행하십시오.


$ rm app.test.js logo.svg app.css && touch User.js Message.js Frontpage.js Message-Query.js User-Query.js 


몇가지 의존성을 설치하자 :


npm i graphql react-apollo apollo-link-ws subscriptions-transport-ws apollo-cache-inmemory 


구독을 구현하지 않았다면 Apollo-boost가 충분했을 수 있습니다. 그러나 구독의 경우 Apollo-boost가 기본적으로 제공하지 않는 일부 하위 수준 기능을 구현해야 하므로 대신 Apollo Client를 사용합니다. 채팅 메시지를 전달하는 데 사용할 구독에 대한 WebSocket 링크, 서버와의 다른 통신을 위한 HTTP 링크 및 Apollo Client를 초기화하는 것으로 시작하겠습니다. 다음과 같이 index.js 파일에서 이 작업을 수행합니다.


//index.js

import React from "react";
import ReactDOM from "react-dom";
import ApolloClient from "apollo-client";
import { ApolloProvider } from "react-apollo";
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
import { InMemoryCache } from 'apollo-cache-inmemory'
import "./index.css";
import App from "./App";

const httpLink = new HttpLink({
  uri: 'http://localhost:4000/'
});

const wsLink = new WebSocketLink({
  uri: `ws://localhost:4000/`,
  options: {
    reconnect: true
  }
});

const link = split(
  ({ query }) => {
    const { kind, operation } = getMainDefinition(query);
    return kind === 'OperationDefinition' && operation === 'subscription';
  },
  wsLink,
  httpLink,
);

const client = new ApolloClient({
  link,
  cache: new InMemoryCache()
})

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById("root")
);
HTTP 링크와 WebSocket 링크가 모두 있습니다. 쿼리 및 변이는 HTTP를 통해 이루어지며 구독은 ws 프로토콜을 통해 수행됩니다. 각 요청을 처리 할 적절한 링크로 분할합니다. 구독 인 경우 split은 WebSocket의 인스턴스인 경우 wsLink 경로를 통해 요청을 보냅니다. 그렇지 않으면 요청이 HTTP를 통해 전송됩니다. Apollo Client를 React 앱에 연결하기 위해 Apollo Provider 구성 요소를 사용합니다. 그러면 React 컴포넌트가 래핑되고 클라이언트가 컨텍스트에 배치되고 앱의 어느 곳에서나 제공됩니다. GraphQL 데이터가 필요한 어느 곳에서나 React 앱을 Apollo Provider로 높게 포장하는 것이 가장 좋습니다. 이것이 index.js 파일에서 이 작업을 수행하는 이유입니다.


//User-Query.js
import gql from "graphql-tag";

const UserQuery = gql`
  query {
    users {
      id
      name
      email
      messages {
        message
        senderMail
        receiverMail
      }
    }
  }
`;

const CreateUserMutation = gql`
  mutation($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      name
      email
      id
      messages {
        message
        senderMail
        receiverMail
      }
    }
  }
`;

//write DeleteUserMutation here

//write addUserSubscription here

export {
  UserQuery,
  CreateUserMutation,
  DeleteUserMutation,
  AddUserSubscription,
  DeleteUserSubscription
};

위에 표시된 User-Query.js는 모든 사용자 쿼리, 변이 및 구독에 대한 GraphQL 쿼리 문자열을 정의하는 곳입니다. 표시되지 않은 것은 모두 UserQuery의 패턴을 따릅니다. 우리는 우리가 서버로 보내는 것과 정의한 것을 정의합니다. 이 매개 변수는 서버의 스키마에서 정의되었습니다. 구독, 쿼리 및 변이를 gql 태그로 래핑하고 각 매개 변수를 정의합니다. gql 함수는 작성한 쿼리 문자열을 GraphQL AST (Abstract Syntax Tree)로 구문 분석합니다. 우리가 작성한 쿼리 문자열이 GraphQL AST로 성공적으로 구문 분석되면 유효성을 검사하고 응답을 보낼 수 있습니다.


여기에서 사용자 쿼리, 변이 및 구독을 구현할 App.js 파일로 이동하겠습니다. react-apollo에서 가져온 compose 및 graphql 상위 구성 요소를 사용하여 Apollo를 React 애플리케이션에 연결합니다.


//App.js

const App = props => {
  const user  = (localStorage.getItem('token') && JSON.parse(localStorage.getItem('token'))) || {}

//useState Hook for using state
  const [receiverState, setReceiverState] = useState({
    receiverMail: "",
    receiverName: ""
  });

//function for updating receiverState
  const setSelectedMail = (mail, user) => {
    setReceiverState(receiverState => {
      return { ...receiverState, receiverMail: mail, receiverName: user };
    });
  };

//useEffect Hook
  useEffect(() => {
    //write user added subscription here
    //write user deleted subscription here
  }, [props.data]);

  const createUser = async (email, name) => {
    await props.createUser({
      variables: {
        email,
        name
      },
      update: (store, { data: { createUser } }) => {
        const data = store.readQuery({ query: UserQuery });
        if (!data.users.find(x => x.id === createUser.id)) {
          data.users.push(createUser);
        }
        store.writeQuery({ query: UserQuery, data });
      }
    });
  };
  
  //write delete user function here
  
  const {
    user: { users, error, loading }
  } = props;

  if (loading || error) return null;

    return (
        <User
          users={users}
          email={user.email}
          name={user.name}
          selectedMail={setSelectedMail}
          deleteUser={deleteUser}
        />
        <Message
          email={user.email}
          receiverMail={receiverMail}
          receiverName={receiverName}
          userLeft={userLeft}
          name={user.name}
    );
 
};

export default compose(
  graphql(UserQuery, { name: "user" }),
  graphql(CreateUserMutation, { name: "createUser" }),
  graphql(DeleteUserMutation, { name: "deleteUser" })
)(App);

여기서 많은 일들이 일어나고 있습니다. 먼저 인증을 수행하지 않기 때문에 로그인 한 사용자를 로컬 저장소에 저장하고 사용자가 다시 방문 할 때마다 가져옵니다. 그런 다음 수신자 이름과 수신자 메일의 상태를 가져오고 설정하기 위해 useState 후크를 사용합니다.


구독은 useEffect 후크에 설정됩니다. 이 후크는 componentDidMount, componentDidUpdatecomponentWillUnmount의 기능을 모두 결합하여 제공합니다. 구독을 설정하기에 이상적인 장소입니다. 듣고 자하는 구독을 정의하기 위해 subscibeToMore 함수를 사용할 것입니다. subscribeToMore를 사용하여 청취 할 구독을 정의한 다음 서버에서 매개 변수를 지정한 경우 인수를 전달할 수 있습니다. addUserSubscription의 경우 새 사용자를 확보하여 모든 사람에게 즉시 브로드 캐스트 하려고 합니다. 사용자가 삭제되면 해당 사용자를 모두에게 브로드 캐스트 하려고 합니다.


다음으로, graphql 상위 컴포넌트에 전달 된 함수를 정의하고 서버가 이를 실행하는 데 필요한 인수를 전달합니다. 우리는 Apollo store에서 낙관적인 UI 업데이트를 제공 할 것입니다. createUser 함수를 사용하여 새 사용자를 만들면 데이터가 서버로 전송 될뿐만 아니라 클라이언트에 즉시 돌연변이가 적용되도록 Apollo 저장소를 업데이트합니다. 작성 기능이 있는 graphql 고차 함수 래퍼를 사용하면 Apollo 클라이언트의 소품으로 앱 구성 요소에 데이터를 주입합니다. 예를 들어, createUserMutation은 props로 제공되고 createUser로 별명을 부여한 다음 함수를 호출하고 함수를 실행할 서버에 정의 된 인수를 제공했습니다.


답장에서 사용자를 사용자 구성 요소로 보냅니다. 우리는 거기에 사용자를 렌더링합니다. 또한 하위 구성 요소에 소품으로 전달되는 콜백 함수 인 setSelectedMail을 정의했습니다. 메시지 구성 요소로 이동하여 메시지 및 userTyping 구독을 설정하는 방법을 알아보십시오.


//Message.js

const Message = props => {
  const chatBox = useRef(null);

  const [message, setMessage] = useState('');

  const [userTyping, setUser] = useState('');

  const [timer, setTimer] = useState(null);

  const handleShow = () => {
    props.setStyle();
  };

  useEffect(() => {
     props.message.subscribeToMore({
      document: MessageSubscription,
      variables: {
        receiverMail: props.email
      },
      updateQuery: (prev, { subscriptionData }) => {
        if (!subscriptionData.data) return prev;
        const msg = subscriptionData.data.newMessage;
        if (prev.messages.find(x => x.id === msg.id)) {
          return prev;
        }
        return { ...prev, messages: [...prev.messages, msg] };
      }
    });
    props.message.subscribeToMore({
      document: UserTypingSubscription,
      variables: {
        receiverMail: props.email
      },
      updateQuery: (prev, { subscriptionData }) => {
        if (!subscriptionData.data) return prev;
        const user = subscriptionData.data.userTyping;
        setUser(user);
      }
    });
    if (chatBox.current) {
      scrollToBottom();
    }
  });

 //write scroll to bottom function

    const handleChange = async e => {
    setMessage(e.target.value);
    const { email, receiverMail } = props;
    await props.userTyping({
      variables: {
        email,
        receiverMail
      }
    });
    const changeMail = async () => {
      await props.userTyping({
        variables: {
          email: "email",
          receiverMail
        }
      });
    };
    clearTimeout(timer);
    setTimer(setTimeout(changeMail, 2000));
  };

//write create user function here
  
//define props here

  return (
    <div className="personal-chat">
      <div className="user-typing">
        {userTyping && userTyping === receiverMail
          ? `${receiverName} is typing`
          : receiverName}
      </div>
      <div className="all-messages">
        {messages.map(item =>
          (item.senderMail === email && item.receiverMail === receiverMail) ||
          (item.senderMail === receiverMail && item.receiverMail === email) ? (
            <div
              key={item.id}
              className={item.users.map(a =>
                a.email === receiverMail ? "receiver" : "sender"
              )}
            >
              <div className="sender-name">{item.users.map(x => x.name)}</div>
              {item.message}{" "}
              <span className="time"> {moment(item.timestamp).fromNow()}</span>
            </div>
          ) : (
            ""
          )
        )}
     //other details
  );
};
//exports here

userTyping 구독을 구현하기 위해 양식의 제어 요소에 대한 React의 onChange 핸들러 기능을 활용할 수 있습니다. 함수를 정의 할 때 입력 요소의 상태를 변경하는 것 이상을 수행하려고 합니다. 우리는 사용자가 입력하는 이메일과 수신 측 사용자의 이메일을 전달합니다. 또한 2000ms 후에 실행되는 setTimeout이 필요합니다. 함수에 더미 인수를 보내서 사용자가 입력을 중지하면 입력 알림이 종료됩니다. 여러 setTimeout 함수가 누적되지 않도록 하기 위해 clearTimeout으로 지 웁니다.


서버에서는 withFilter() 함수를 사용하여 제공 한 변수의 receiverMail이 userTyping 변이의 페이 로드에 있는 receiverMail과 동일한 경우에만 구독을 가져 오도록 지정했습니다. handleChange() 함수에서 페이 로드를 전달했습니다. useEffect Hook에서 이 변수를 전달할 것입니다. 이 후크에서 변수 인 receiverMail을 정의하고 이에 대한 이메일을 할당했음을 알 수 있습니다. 논리는 이것입니다. 입력 할 때 사용자의 email-receiverMail을 지정하여 메시지를 보내는 사용자에게 서버에 알립니다. 서버에서 내 전자 메일을 반환하고 싶지만 이 사용자가 내 전자 메일을 receiverMail로 가지고 있는 경우에만 나와 채팅하는 사용자 일 것입니다. 이를 통해 채팅 중인 사용자가 입력 중인 경우에만 알림을 받습니다.


답장에서는 간단히 확인하고 사용자 이메일이 senderMail 또는 receiverMail 인 메시지를 표시합니다. 모든 사용자는 자신이 보내거나 받은 메시지를 받습니다.


위에서 언급 한 사용자를 렌더링 하는 구성 요소 외에, 앱에 남겨진 유일한 것은 사용자가 로그인에 사용할 수 있는 양식이 있는 첫 페이지입니다. 우리는 응용 프로그램 구성 요소에서 콜백을 전달하여 사용자가 이 양식을 제출하면 앱 컴포넌트의 createUser 함수가 구현되고 새 사용자가 작성됩니다. 자세한 내용을 보려면 GitHub에서 리포지를 신속하게 확인하십시오.

완전한 기능의 실시간 채팅 응용 프로그램이 작동 중입니다. 나는 당신이 이것을 즐겼기를 바랍니다. 확실해 너무 길고 지루하지 않게 하려면 여기서 멈춰야 합니다.


npm start를 실행하고 localhost : 3000으로 가서 즐거운 시간을 보내십시오.