[유데미x스나이퍼팩토리] 10주 완성 프로젝트 캠프 - 무비 앱에 실시간 채팅기능 구현 & 커스텀 훅 만들기

강경서·2023년 7월 2일
0
post-thumbnail

🎬 Movie App 에 실시간 채팅기능 구현

Websocket 프로토콜을 사용하여 Movie App 에 실시간 채팅기능 구현


Server

Githup 링크

  • server.js
const express = require("express");
const http = require("http");
const socketIO = require("socket.io");
const cors = require("cors");

const app = express();
app.use(cors());
const server = http.createServer(app);
const io = socketIO(server, {
  cors: {
    origin: "http://localhost:3000",
    methods: ["GET", "POST"],
  },
});

io.on("connection", (socket) => {
  console.log("새로운 클라이언트가 연결되었습니다.");

  socket.on("message", (message) => {
    console.log("새로운 메시지:", message);
    io.emit("message", message);
  });

  // 클라이언트 연결 해제 처리
  socket.on("disconnect", () => {
    console.log("클라이언트가 연결을 해제하였습니다.");
  });
});

server.listen(3001, () => {
  console.log("서버가 3001 포트에서 실행 중입니다.");
});

Express 프레임워크와 Socket.io 라이브러리를 이용하여 WebSocket 서버를 만들었습니다.
WebSocket 서버는 on 메서드를 통해 클라이언트의 접속인 "connection"을 받습니다. 클라이언트와 연결시 socket 객체를 받으며, socket 객체는 on 메소드를 통해 데이터를 받습니다. emit 메서드는 클라이언트에게 데이터를 전달 할 수 있습니다.


Chat

Github 링크

  • src/component/Chat.js
import React, { useState, useEffect } from "react";
import { io } from "socket.io-client";
import Styles from "styles/Chat.module.css";

const socket = io("http://localhost:3001");

function Chat() {
  const [username, setUsername] = useState("");
  const [messages, setMessages] = useState([]);
  const [inputValue, setInputValue] = useState("");
  const [chatDisplay, setChatDisplay] = useState(false);

  useEffect(() => {
    socket.on("message", handleMessage);
    return () => {
      socket.off("message", handleMessage);
    };
  }, []);

  const handleMessage = (message) => {
    setMessages((pre) => [...pre, message]);
  };

  const handleMessageSubmit = (evnet) => {
    evnet.preventDefault();
    if (inputValue.trim() !== "") {
      const currentTime = new Date().toLocaleDateString();
      socket.emit("message", {
        username,
        content: inputValue,
        time: currentTime,
      });
      setInputValue("");
    }
  };

  return (
    <>
      <div
        className={Styles.chat}
        style={{ display: chatDisplay ? "flex" : "none" }}
      >
        <div className={Styles.chatContainer}>
          <div className={Styles.chatHeader}>
            <div className={Styles.chatTitle}>Chat</div>
            <div
              className={Styles.chatCancle}
              onClick={() => setChatDisplay(false)}
            >
              𝖷
            </div>
          </div>
          <div className={Styles.chatList}>
            {messages.map((message, index) => (
              <p key={index}>
                {message.username} : {message.content} - {message.time}
              </p>
            ))}
          </div>
          <form className={Styles.chatForm}>
            <input
              type="text"
              value={username}
              onChange={(event) => setUsername(event.target.value)}
              placeholder="사용자 이름"
              className={Styles.chatName}
            />
            <input
              type="text"
              value={inputValue}
              onChange={(event) => setInputValue(event.target.value)}
              placeholder="메세지"
              className={Styles.chatMessage}
            />
            <input
              type="submit"
              onClick={handleMessageSubmit}
              value={"전송"}
              className={Styles.chatSubmit}
            />
          </form>
        </div>
      </div>
      <div
        className={Styles.chatBtn}
        onClick={() => setChatDisplay(true)}
        style={{ display: !chatDisplay ? "block" : "none" }}
      >
        Chat
      </div>
    </>
  );
}

export default Chat;

Chat 컴포넌트는 useEffect를 통해 Websocket 서버의 이벤트를 받습니다.클라이어트 내에서도 on 메서드와 emit 메서드를 이용하여 서버와 데이터를 주고 받을 수 있습니다.


결과


🧷 커스텀 훅 만들기

  • useFetchMovies Custom Hook 을 설계, 구현, 문서화 해보세요.
  • useFavoriteMovies Custom Hook 을 설계, 구현, 문서화 해보세요.
  • useSearchMovies Custom Hook 을 설계, 구현, 문서화 해보세요.
  • usePagination Custom Hook 을 설계, 구현, 문서화 해보세요.

useFetchMovies

  1. useFetchMovies Custom Hook 을 설계, 구현, 문서화 해보세요 😊
  2. 영화 API에 HTTP 요청을 보내고, 응답을 받아와서 영화 데이터를 처리
  3. 반환값으로는 로딩 상태, 영화 데이터, 에러 정보 등을 포함하는 객체를 반환

Github 링크

  • src/libs/useFetchMovies.js
import React, { useEffect, useState } from "react";

/**
 * 영화 데이터를 Fetch하는 커스텀 훅입니다.
 * @param {string} url - movie API
 * @returns {object} - { movies, loading, error } 영화 데이터, 로딩 상태, 에러 정보를 포함하는 객체를 반환합니다.
 */

const useFetchMovies = (url) => {
  const [movies, setMovies] = useState();
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState();

  const getMovies = async () => {
    try {
      const response = await fetch(url);
      const data = await response.json();
      setMovies(data);
    } catch (error) {
      setError(error);
    }
  };

  useEffect(() => {
    getMovies();
  }, []);

  useEffect(() => {
    if (movies) setLoading(false);
  }, [movies]);

  return { movies, loading, error };
};

export default useFetchMovies;

movie API를 받아 Fetch 하는 훅이므로 parameter에 컴포넌틑에 필요한 데이터를 반환하는 movie API를 받게 하였습니다.

return 값으로는 영화 데이터, 로딩 상태, 에러 정보를 포함하는 객체를 반환합니다.

초기의 로딩 상태는 true로 반환하여 로딩중임을 표현하고, 데이터 응답을 받아 영화 데이터 상태가 변경됬을경우 로딩 값을 false로 변경하여 로딩이 끝났음을 표현했습니다.

error 상태또한 fetch가 실패하여 error가 생길경우 try-catch를 사용하여 error 상태를 관리하였습니다.


useFavoriteMovies

  1. useFavoriteMovies Custom Hook 을 설계, 구현, 문서화 해보세요 😊
  2. 사용자가 영화를 즐겨찾기에 추가하거나 제거할 수 있는 함수를 제공합니다.
  3. 반환값 자유

Github 링크

  • src/libs/useFavoriteMovies.js
import React from "react";

/**
 * 사용자가 영화를 즐겨찾기에 추가하거나 제거할 수 있는 함수를 제공하는 커스텀 훅입니다.
 * @param {any} id - movie_id
 * @param {any} userKey - user_key
 * @returns {object} - { addMovie, deleteMovie } 영화를 즐겨찾기에 추가하거나 제거할 수 있는 함수를 반환합니다.
 */

const useFavoriteMovies = (id, userKey) => {
  const applicationKey = "key";
  const addMovie = async () => {
    await fetch(
      `https://yts.mx/api/v2/add_movie_bookmark.json?user_key=${userKey}&movie_id=${id}$application_key=${applicationKey}`,
      { method: "POST" }
    );
  };
  const deleteMovie = async () => {
    await fetch(
      `https://yts.mx/api/v2/delete_movie_bookmark.json?user_key=${userKey}&movie_id=${id}$application_key=${applicationKey}`,
      { method: "POST" }
    );
  };
  return { addMovie, deleteMovie };
};

export default useFavoriteMovies;

사용자가 영화를 즐겨찾시에 추가하거나 제거할 수 있는 API는 movie id, user key, application key값이 필요합니다. 여기서 movie id와 user key를 커스텀 훅의 parameter로 설정하였습니다. 이는 영화와 로그인한 유저에 따라 해당값이 달라지기 때문입니다.

return 값으로는 영화를 즐겨찾기에 추가하거나 제거할 수 있는 함수를 반환합니다. 해당 함수들은 커스텀 훅의 parameter로 받은 movie id와 user key를 템플릿 리터럴이용하여 가각의 API 필요한 값을 추가하였습니다.


useSearchMovies

  1. useSearchMovies Custom Hook 을 설계, 구현, 문서화 해보세요 😊
  2. 사용자가 입력한 검색어로 영화를 검색하고, 검색 결과를 처리합니다.
  3. 반환값 자유

Github 링크

  • src/libs/useSearchMovies.js
import React, { useEffect, useState } from "react";

/**
 * 사용자가 입력한 검색어로 영화를 검색하고, 검색 결과를 처리하는 커스텀 훅입니다.
 * @param {string} query - query
 * @returns {object} - { movies, loading, error } 영화 데이터, 로딩 상태, 에러 정보를 포함하는 객체를 반환합니다.
 */

const useSearchFetchMovies = (query) => {
  const [movies, setMovies] = useState();
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState();

  const getMovies = async () => {
    try {
      const response = await fetch(
        `https://yts.mx/api/v2/list_movies.json?query_term=${query}`
      );
      const data = await response.json();
      setMovies(data);
    } catch (error) {
      setError(error);
    }
  };

  useEffect(() => {
    if (!query) return;
    setLoading(true);
    getMovies();
  }, [query]);

  useEffect(() => {
    if (movies) setLoading(false);
  }, [movies]);

  return { movies, loading, error };
};

export default useSearchFetchMovies;

useFetchMovies와 비슷한 작업을 하지만 API로 검색어로 영화를 검색하는 API를 하나만 사용하기 때문에 검색에 따라 달라지는 query를 parameter로 받습니다.

return 값으로는 영화 데이터, 로딩 상태, 에러 정보를 포함하는 객체를 반환합니다.

초기의 로딩 상태는 true로 반환하여 로딩중임을 표현하고, 데이터 응답을 받아 영화 데이터 상태가 변경됬을경우 로딩 값을 false로 변경하여 로딩이 끝났음을 표현했습니다.

error 상태또한 fetch가 실패하여 error가 생길경우 try-catch를 사용하여 error 상태를 관리하였습니다.

useEffect를 이용하여 query값이 변경될 때마다 API를 Fetch하는 함수를 재실행시킵니다.
이때 로딩 상태를 true로 변경하여 로딩중임을 표현합니다.


usePagination

  1. usePagination Custom Hook 을 설계, 구현, 문서화 해보세요 😊
  2. 사용자가 다른 페이지로 이동할 수 있도록 페이지 번호를 관리합니다.
  3. 반환값 자유

Github 링크

  • src/libs/usePagination.js
import React, { useEffect, useState } from "react";

/**
 * 사용자가 다른 페이지로 이동할 수 있도록 페이지 번호를 관리하는 커스텀 훅입니다.
 * @param {array} data - 영화 데이터 배열
 * @param {number} count - 페이지당 보여지는 영화의 개수
 * @returns {object} - { page, nextPage, prevPage, movePage, maxPage, sliceData } 현재 페이지, 이전 페이지 이동 함수, 다음 페이지 이동 함수, 해당 페이지 이동 함수, 전체 페이지 개수, 나눠진 영화 배열
 */

const usePagination = (data, count) => {
  const [page, setPage] = useState(1);
  const [maxPage, setMaxPage] = useState();
  const [sliceData, setSliceData] = useState();

  const nextPage = () => {
    if (page < maxPage) {
      setPage((pre) => pre + 1);
      window.scrollTo({ top: 0 });
    }
  };
  const prevPage = () => {
    if (page > 1) {
      setPage((pre) => pre - 1);
      window.scrollTo({ top: 0 });
    }
  };
  const movePage = (order) => {
    setPage(order);
    window.scrollTo({ top: 0 });
  };

  useEffect(() => {
    if (!data) return;
    setMaxPage(Math.round(data.length / count));
    setPage(1);
  }, [data, count]);

  useEffect(() => {
    if (!data) return;
    setSliceData(data.slice((page - 1) * count, page * count));
  }, [data, page, count]);

  return { page, nextPage, prevPage, movePage, maxPage, sliceData };
};

export default usePagination;

usePagination 커스톰 훅은 영화 데이터 배열과 한 페이지당 보여주는 영화의 개수를 parameter로 받습니다. 영화 데이터는 API를 통해 50개로 이루어지진 배열을 받기 때문에 한 페이지당 보여주는 영화의 개수를 통해 전체 페이지 수를 알 수 있습니다.

현재 페이지 값은 useState을 통해 초기값을 설정 후 nextPage, prevPage, movePage 함수를 통해 페이지를 이동시킵니다.

한 페이지당 보여주는 영화의 개수와 현재 페이지 값을 이용하여 영화 데이터 배열을 slice 메서드를 통해 데이터를 잘라 상태를 저장합니다.


📝 후기

Socket.io를 통해 WebSocket 프로토콜을 만들어 실시간 채팅 기능을 구현할 수 있었습니다. Socket.io를 통해 Room 또한 구현이 가능하다는데 이를 채팅 기능에 추가하면 좋을거 같다는 생각을 했습니다.

다양한 커스텀 훅을 만들어보면서 다양한 시도를 해보았습니다. 어떤 상태와 이벤트 핸들러를 관리해야 하는지, 어떤 초기값을 받아와야 하는지, 어떤 값을 반환해야 하는지 등을 생각하면서 영화 앱에 필요한 커스텀 훅을 만들어보았습니다. 아직 부족한 부분이 많이 보여 수정을 통해 기능을 개선하고 싶습니다.



본 후기는 유데미-스나이퍼팩토리 10주 완성 프로젝트캠프 학습 일지 후기로 작성 되었습니다.

#프로젝트캠프 #프로젝트캠프후기 #유데미 #스나이퍼팩토리 #웅진씽크빅 #인사이드아웃 #IT개발캠프 #개발자부트캠프 #리액트 #react #부트캠프 #리액트캠프

profile
기록하고 배우고 시도하고

0개의 댓글