분류 Reactjs

Node.js, GraphQL 및 React로 실시간 채팅 애플리케이션을 구축하는 방법 — 1

컨텐츠 정보

  • 조회 414 (작성일 )

본문

소개 


우리는 함께 공통점을 탐색 할 것입니다. 우리는 멋진 기술을 사용하여 유용한 도구를 만들어 내고 소프트웨어 개발이 제공하는 재미를 일부 포착 할 것입니다.


Node.js, GraphQL, MongoDB를 사용하여 실시간 채팅 애플리케이션을 구축 할 것입니다. 예, 완전히 새롭고 반짝이는 React Hooks를 사용하고 있습니다. 우리의 응용 프로그램은 사용자가 로그인하고 모든 로그인 된 사용자를 볼 수 있도록하여 사용자가 선택하고 대화 할 수 있습니다. 또한 새로운 사람들이 채팅에 참여하거나 채팅중인 사람들이 입력 할 때 알림을 받습니다. 


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


백엔드 웹 개발을 배우기 시작했을 때 나는 순전히 GraphQL에 들어갔다. YouTube에서 전체 스택 자습서를 검색했으며 가장 최근에 GraphQL을 사용했습니다. 나는 GraphQL이 무엇인지 또는 대안이 무엇인지 모른 채 튜토리얼을 가져갔습니다. 말할 것도 없이 내 학습은 여러 단계에서 피할 수 없는 어려움으로 가득 차 있었습니다. 나는 GraphQL 문서의 다른 페이지를 읽고 많은 질문에 대해 StackOverflow에서 사용 가능한 몇 가지 답변을 조사하는 데 많은 시간을 보냈습니다. GraphQL에는 재료가 제공하고 제공하는 많은 약속에도 불구하고 재료가 상대적으로 부족하다는 사실에 충격을 받았습니다. 이 글을 쓰는 것은 이 훌륭한 개발자 커뮤니티에 돈을 지불하는 좋은 방법입니다. 그래서 훌륭한 독자는 당신보다 더 빨리 배울 수 있습니다.


이 튜토리얼이 필요 이상으로 길어지는 것을 피하기 위해 기술 스택에 대해 이야기하는 데 많은 시간을 할애하지 않습니다. 그렇게 하는 훌륭한 자료가 이미 많이 있습니다. 이 튜토리얼은 두 부분으로 나뉩니다. 이것이 첫 번째이며 서버 설계를 다룰 것입니다. 두 번째 자습서에서는 앱의 프런트 엔드를 작성합니다.


행동을 시작합시다. 이 앱의 코드는 GitHub에 있습니다. 저장소를 복제하고 코드를 확인할 수 있습니다.


https://github.com/LukasChiama/chat_app_server/tree/master?source=post_page-----d06cfbc52c8c----------------------


이것은 매우 간단한 앱이며 간단한 파일 구조를 사용합니다. 결국 서버 폴더는 다음과 같습니다.


-server
  -index.js
  -models.js
  -typeDefs.js
  -resolvers.js

설정 서버 


GraphQL-yoga를 사용하겠습니다. 설치가 매우 쉬운 완전한 기능을 갖춘 GraphQL 서버입니다. GraphQL은 Facebook의 사람들이 REST의 대안으로 개발한 API용 쿼리 언어입니다. 또한 이러한 쿼리를 수행하기 위한 런타임이기도 합니다. GraphQL은 클라이언트가 서버에서 데이터베이스에 필요한 것을 정확하게 요청할 수 있는 기능을 제공하는 API 설계를 위한 REST 아키텍처의 대안입니다. 이것들은 더 이상 아무것도 얻지 못합니다. 달콤하지 않습니까?


mLab과 Mongoose 라이브러리를 사용합니다. 로컬 인스턴스를 사용하도록 로컬로 설치하려면 MongoDB 공식 문서를 참조하십시오. 이 앱은 Heroku에서 호스팅되므로 mLab을 사용할 것입니다. mLab는 MongoDB의 데이터베이스 서비스 제공 업체입니다. 웹에서 무료 MongoDB 데이터베이스 인스턴스를 제공합니다. mLab 설정에 도움이 필요한 경우 이를 확인할 수 있습니다.


이제 npm을 초기화하고 프로젝트에 필요한 종속성을 설치하십시오.


$ npm init -y
$ npm i graphql-yoga mongoose 


index.js 파일에서 이러한 종속성을 가져옵니다.


//index.js
const { PubSub, withFilter, GraphQLServer } = require("graphql-yoga");
const mongoose = require("mongoose")

GraphQLServer는 graphql-yoga가 제공하는 핵심 프리미티브이며 마법이 일어날 서버 인스턴스를 만듭니다. 이 서버는 기본 서버 구성뿐만 아니라 GraphQL 스키마와 관련된 모든 항목으로 구성됩니다. PubSub 및 withFilter는 앱을 실시간으로 만드는 GraphQL 구독에 사용됩니다. 우리는 이것들에 대해 조금 알게 될 것입니다.


다음으로 mongoose를 데이터베이스에 연결하십시오. 처음 두 개의 인수를 통과하지 않으면 Mongoose에서 지원 중단 경고가 표시됩니다.


//index.js
mongoose.connect("mongodb://localhost/miniChat", {
  useNewUrlParser: true,
  useFindAndModify: false,
  useCreateIndex: true
});


Models 


다음으로 몽구스 모델을 만들고 스키마를 전달하십시오. 이것은 간단한 앱이므로 사용자와 메시지의 두 가지 모델만 필요합니다. models.js 파일에 이것을 작성해 봅시다 :


//models.js
const User = mongoose.model("User", {
  name: {
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true,
    index: { unique: true }
  }
});

const Message = mongoose.model("Message", {
  message: String,
  senderMail: String,
  receiverMail: String,
  timestamp: Number
});

이메일을 사용하여 사용자를 식별하기 때문에 이메일이 독창적이기를 바랍니다. 또한 이름과 이메일 필드를 모두 강제 해야 합니다.


TypeDefs 


GraphQL 서버에 스키마를 제공하거나 typeDefs 및 resolver를 사용할 수 있습니다. 여기서는 후자를 사용하겠습니다. TypeDefs는 원하는 스키마 모델을 정의하고 각 모델에 포함해야 하는 것을 지정합니다. 이들은 GraphQL API의 구조를 정의하고 GraphQL SDL (스키마 정의 언어)로 작성됩니다.

또한 사용하려는 GraphQL 작업 유형을 정의합니다. 쿼리, 변이 및 구독이라는 세 가지 작업 유형이 있습니다. 쿼리 및 변이는 스키마의 진입점으로 특별한 위치를 차지합니다. 쿼리는 데이터 요청에 사용됩니다. CRUD 기능에서 R-Read를 수행합니다. 변이는 변이입니다.

그것들은 우리가 데이터를 변경하도록 허용합니다. Haha… 변이를 통해 모델의 데이터를 수정할 수 있습니다. C-Create, U-Update 및 D-Delete 기능에 사용됩니다. 반면에 가입은 서버와의 실시간 연결을 만들고 유지 관리하는 데 사용됩니다. 자세한 내용은 이 정보를 참조하십시오. typeDefs.js 파일을 작성하고 이 코드를 작성하십시오. 백틱을 잊지 마십시오.


//typeDefs.js
const typeDefs = `
  type Query {
    users: [User]
    messages: [Message]
  }

  type User {
    id: ID!
    name: String!
    email: String!
    messages: [Message]
  }

  type Message {
    id: ID!
    message: String!
    senderMail: String!
    receiverMail: String!
    timestamp: Float!
    users: [User]
  }

 type Mutation {
    createUser(name: String! email: String!): User!
    updateUser(id: ID! name: String!): User!
    deleteUser(email: String!): Boolean!
    userTyping(email: String! receiverMail: String!): Boolean!

    createMessage(senderMail: String! receiverMail: String! message: String! timestamp: Float!): Message!
    updateMessage(id: ID! message: String!): Message!
    deleteMessage(id: String!): Boolean!
  }

  type Subscription {
    newMessage(receiverMail: String!): Message
    userTyping (receiverMail: String!): String
    newUser: User
    oldUser: String
  }`;

쿼리, 변이 및 구독 작업 유형 뿐만 아니라 사용자 및 메시지 유형도 정의했습니다. 이들은 모두 객체 유형이며 GraphQL 스키마의 가장 기본 구성 요소입니다. 이를 통해 서버에서 가져올 수 있는 것을 정의 할 수 있습니다. 이는 언어에 구애 받지 않으므로 Node.js 백엔드로 제한되지 않습니다.


사용자는 메시지 필드를 포함합니다. 이것은 우리가 쿼리 하는 특정 사용자와 관련된 메시지 배열을 제공합니다. 메시지에는 메시지를 작성한 사용자에 대한 참조도 포함됩니다. 우리는 이것을 사용하여 사용자가 보내는 메시지에 사용자를 연결합니다.


우리의 변이는 우리가 원하는 필드와 우리가 변이에서 돌려주고 싶은 것을 지정합니다. createUser 변이의 경우 사용자가 문자열인 이름을 제공하기를 원합니다. 느낌표 (!)는 이것이 필수 필드이며 널이 될 수 없음을 나타냅니다. 이 필드를 제공하지 않으면 새 사용자가 생성되지 않습니다. 또한 문자열인 필수 이메일을 원합니다.

작업이 성공하면 생성 된 사용자가 반환되기를 원합니다. 그것이 바로 사용자입니다! 괄호 밖에 있습니다. 변이에서 반환 된 값을 지정합니다. 다른 변이는 동일한 논리를 따릅니다. ID를 만들지는 않았지만 일부 변이에는 ID가 필요합니다. 이것은 생성 된 모델의 모든 인스턴스에 대해 id가 자동으로 생성되기 때문에 가능합니다.


구현하기 위한 멋진 기능은 상대방이 입력 할 때 알림 채팅 응용 프로그램이 제공하는 것입니다. 그리고 우리는 그것을 구현할 것입니다! 네! 이것이 바로 사용자 타이핑 변이가 하는 일입니다. 우리는 구독으로 들을 수 있는 변이를 만들고 있습니다. 구독이 작동하려면 입력하는 사용자와 메시지의 사용자를 알아야 합니다. 그래서 우리는 userTyping 변이에 이메일과 수신자 메일을 전달합니다. 


newMessage 구독의 경우, 우리가 제공하는 수신자 메일과 연관된 모든 새 메시지를 가져 오려고 합니다. 모든 사용자가 다른 사람이 보내는 새 메시지를 받는 것을 원하지 않습니다. 채팅 중인 사용자로부터 만 메시지를 받습니다. 우리는 서버가 우리가 제공 한 이메일을 가진 사람이 보내는 메시지를 보내도록 지시 할 것입니다. 또한 userTyping 구독에 대해서도 동일한 작업을 수행합니다. 이를 통해 채팅하는 사람이 언제 메시지를 보냈는지 뿐만 아니라 입력 할 때도 알 수 있습니다. 그러나 newMessage의 경우 메시지를 반환하기를 원합니다. 입력시 식별을 위해 이메일만 반환 됩니다.


newUser 가입의 경우 모든 새 사용자가 생성되면 즉시 반환됩니다. 이 구독을 통해 앱을 사용하는 사람은 누구나 새로 가입 한 사용자를 즉시 ​​볼 수 있습니다. oldUser 가입은 앱에서 삭제 된 사용자를 제거합니다. 이 두 가지에 대해 앱의 모든 사용자에게 알리기를 원하므로 매개 변수가 필요하지 않습니다.


Query Resolvers 


이제 리졸버에게 resolvers.js를 만들고 이것을 다음과 같이 작성하십시오 :


//resolvers.js
const resolvers = {
  Query: {
    users: () => User.find(),
    messages: () => Message.find()
  },
  
}

리졸버는 typeDefs에 정의 된 객체 유형에 대한 함수입니다. 앞서 typeDefs가 구축 중인 GraphQL API의 구조를 정의한다고 언급 했습니까? 리졸버는 API의 구현입니다. typeDefs에 이미 지정된 모든 객체 유형에 대한 리졸버를 정의합니다. 위 요점의 검색어는 검색어 카테고리의 모든 항목을 검색하여 반환하는 것입니다. 사용자 또는 메시지에서 쿼리를 실행하면 해당 데이터베이스 항목이 모두 반환 됩니다.


사용자 또는 메시지 쿼리의 경우 관련 콘텐츠를 찾아 쿼리를 실행할 때 반환해야 합니다. 사용자가 만든 모든 메시지를 동일한 사용자에게 첨부하거나 그 반대로 첨부하려고 합니다. 사용자의 경우 사용자의 이메일을 사용하여 메시지를 검색하고 senderMail과 동일한 이메일을 가진 메시지를 반환하려고 합니다. 마찬가지로 메시지 쿼리에서 각 개별 메시지 전송을 담당하는 사용자를 찾고 싶습니다. 이것은 위에서 이미 작성한 쿼리 필드와 마찬가지로 여전히 리졸버의 일부임을 기억하십시오. 이 코드를 작성하십시오 :


//resolvers.js
User: {
  messages: async ({ email }) => {
    return Message.find({ senderMail: email });
  }
},

Message: {
  users: async ({ senderMail }) => {
    return User.find({ email: senderMail });
  }
},

Mutation Resolvers 


우리 돌연변이에 대한 리졸버에게. typeDefs에서 요청한 값을 가져 와서 새 객체를 생성하거나 기존 객체를 수정하거나 삭제합니다. 이러한 데이터베이스 상호 작용을 위해 비동기 함수를 사용하여 I / O 호출을 차단하지 않습니다. 우리의 비동기 함수는 우리가 기다릴 약속을 돌려 줄 것입니다. 콜백을 사용할 수 있지만 이 방법이 더 깔끔하지 않습니까? 이 앱은 프런트 엔드에서 업데이트 기능을 구현하지 않습니다. 여기에는 필요한 다른 앱을 개발하는 데 도움이 되는 가이드가 있습니다. 리졸버는 obj, args, context 및 info의 네 가지 인수를 사용합니다. 이 앱에는 args- argument 만 사용하면 됩니다.


새 사용자 나 메시지가 생성 된 후 사용자가 삭제되거나 사용자가 입력 할 때 서버가 알려주도록 해야 합니다. 이것이 바로 pubsub.publish() 함수가 아래의 변이에 대해하는 것입니다. 게시 및 구독 API를 제공하여 이 필드에 추가 한 내용을 구독하고 해당 필드를 클라이언트에 게시 할 수 있습니다. 이러한 돌연변이에 대한 리졸버가 호출 될 때 pubsub도 호출되며 우리가 지정한 세부 정보를 클라이언트에 게시합니다. 사용자가 삭제되면 사용자가 보낸 모든 메시지를 찾은 다음 삭제합니다. 이를 위해 Promise.all() 함수를 사용하여 사용자 및 메시지 삭제를 기다립니다.


//resolvers.js
  Mutation: {
    createUser: async (_, { name, email }) => {
      const user = new User({ name, email });
      await user.save();
      pubsub.publish("newUser", { newUser: user });
      return user;
    },

    updateUser: async (_, { id, name }) => {
      const user = await User.findOneAndUpdate(
        { _id: id },
        { name },
        { new: true }
      );
      return user;
    },

    deleteUser: async (_, { email }) => {
      await Promise.all([
        User.findOneAndDelete({ email: email }),
        Message.deleteMany({ senderMail: email })
      ]);
      pubsub.publish("oldUser", { oldUser: email });
      return true;
    },

    userTyping: (_, { email, receiverMail }) => {
      pubsub.publish("userTyping", { userTyping: email, receiverMail });
      return true;
    },

    createMessage: async (
      _,
      { senderMail, receiverMail, message, timestamp }
    ) => {
      const userText = new Message({
        senderMail,
        receiverMail,
        message,
        timestamp
      });
      await userText.save();
      pubsub.publish("newMessage", {
        newMessage: userText,
        receiverMail: receiverMail
      });
      return userText;
    },

    updateMessage: async (_, { id, message }) => {
      const userText = await Message.findOneAndUpdate(
        { _id: id },
        { message },
        { new: true }
      );
      return userText;
    },

    deleteMessage: async (_, { id }) => {
      await Message.findOneAndDelete({ _id: id });
      return true;
    }
  },


Subscriptions 


구독은 HTTP가 아닌 WebSocket에 의해 제공됩니다. WebSocket은 HTTP와 같은 전이중 통신 채널을 제공하는 컴퓨터 통신 프로토콜입니다. 이렇게 하면 클라이언트가 서버에 메시지를 보낼 수 있는 것과 같은 방식으로 서버가 클라이언트에 메시지를 보낼 수 있습니다. HTTP에서는 서버가 응답을 처리하는 동안 클라이언트가 항상 요청을 시작합니다. HTTP는 단방향이며 WebSocket은 양방향입니다. 데이터베이스에서 새 메시지나 사용자가 작성된 시기를 찾아서 클라이언트에게 정보를 보내는 데 적합합니다.


관심 있는 구독에 대한 리졸버를 작성하겠습니다. pubsub.asyncIterator()를 사용하여 구독 이름을 인수로 전달하여 관심 있는 이벤트를 매핑합니다. GraphQL 엔진의 구독을 정의합니다. 이제 이 구독에서 pubsub.publish()를 호출 할 때마다 pubsub.publish()를 정의한 리졸버가 호출 될 때와 같이 게시 됩니다.

newMessage 및 userTyping 구독에서 withFilter API를 사용합니다. 게시 할 구독을 정의 할 수 있도록 필터 기능을 제공합니다. 우리는 이 메시지를 고객에게 공개하는 것에 만 관심이 있습니다. 클라이언트에서는 특정 구독자에게 푸시 할 구독을 결정하는 인수를 전달합니다. 새로운 사용자와 삭제 된 사용자에 대한 필터는 원하지 않습니다. 우리는 모두가 그들을 보길 원합니다.


//resolvers.js
Subscription: {
    newMessage: {
      subscribe: withFilter(
        () => pubsub.asyncIterator("newMessage"),
        (payload, variables) => {
          return payload.receiverMail === variables.receiverMail;
        }
      )
    },

    newUser: {
      subscribe: (_, {}, { pubsub }) => {
        return pubsub.asyncIterator("newUser");
      }
    },

    oldUser: {
      subscribe: (_, {}, { pubsub }) => {
        return pubsub.asyncIterator("oldUser");
      }
    },

    userTyping: {
      subscribe: withFilter(
        () => pubsub.asyncIterator("userTyping"),
        (payload, variables) => {
          return payload.receiverMail === variables.receiverMail;
        }
      )
    }
  }

결론 


마지막으로 백엔드의 경우 PubSub 인스턴스를 작성하고 서버를 작성하고 시작하십시오. pubsub를 GraphQLServer에 컨텍스트로 전달합니다. 이것은 리졸버에서 서버 액세스 pubsub를 활성화 하기 위한 것입니다.


//index.js
const pubsub = new PubSub();
const server = new GraphQLServer({ typeDefs, resolvers, context: { pubsub } });
mongoose.connection.once("open", () =>
  server.start(() => console.log("We make magic over at localhost:4000"))
);

이 명령으로 서버를 실행하십시오


node index.js 


nodemon을 사용하므로 변경시 서버를 다시 시작할 필요가 없습니다. Nodemon은 index.js 파일을 감시하고 변경 사항을 저장할 때 서버를 자동으로 다시 시작합니다. 다음을 실행하여 nodemon을 개발 종속성 (프로덕션이 아닌 프로덕션에만 있는 종속성)으로 설치하십시오.


npm i -D nodemon 


이제 package.json의 시작 스크립트에 포함하십시오.


"scripts": {"start": "nodemon index.js"},


이제 npm start를 실행하여 서버를 시작할 수 있으며 변경할 때마다 nodemon이 서버를 다시 시작합니다.


확인해야 할 경우 코드는 다음과 같습니다.


https://github.com/LukasChiama/mini_chat_app/blob/master/server/index.js?source=post_page-----d06cfbc52c8c----------------------


브라우저에서 localhost : 4000으로 이동하십시오. GraphQL-yoga를 사용하면 GraphQL API 작업을 위한 IDE 인 GraphQL Playground를 사용할 수 있습니다. 구독을 지원하므로 작성한 모든 내용을 테스트 할 수 있습니다. 축하합니다. 실시간 채팅 응용 프로그램 서버가 실행 중입니다!


1*5VlOx7nyTF21occ5TOVimQ.png