분류 Reactjs

React, Typescript, Node 및 Socket.io로 만든 인스턴트 메시징 앱

컨텐츠 정보

  • 조회 259 (작성일 )

본문

프로젝트 소개 ? 


GroupChat을 소개하게 되어 기쁩니다 ?


이 챌린지의 와이어 프레임은 구축하고 실행할 프로젝트에 대한 많은 멋진 아이디어를 제공하는 devchallenges에서 제공합니다. 영감을 얻지 못했다면 한번보세요!


자, GroupChat에 대해 이야기 해 보겠습니다. 사용자가 채널을 만들고 특정 주제에 관심이 있는 사람들과 채팅 할 수 있는 인스턴트 메시징 앱입니다.


간단하게 들리나요? 글쎄, 나는 그것이 "복잡하다"고 말하지는 않지만 새로운 것을 시도하는 것은 항상 도전적이다.


socket.io로 작업 한 것은 처음이었고 TypeScript로 빌드 된 첫 중형 프로젝트이기도했습니다.


특징 ? 


  • 사용자 정의 인증 (이메일-비밀번호)
  • 게스트로 로그인 (제한된 액세스)
  • 랜덤 아바타 / 프로필 이미지 업로드
  • 승인 (json 웹 토큰)
  • 종단 간 입력 유효성 검사
  • 채널 생성 및 가입
  • 인스턴트 메시징
  • 버그 신고
  • 모바일 친화적

기술 스택 ⚛️ 


다시 한 번, 나는 내 가장 친한 친구 인 MERN 스택을 찾아 갔다.


위의 기술 외에도 TypeScript를 사용하여 코드의 견고성을 개선하고 Redux를 사용하여 앱 상태를 관리했습니다.


또한 브라우저와 서버 간의 실시간, 양방향 및 이벤트 기반 통신을 가능하게 하는 socket.io도 언급해야 합니다.


배포의 경우 쉽고 효율적인 방법은 Netlify에서 프런트 엔드를 호스팅하고 Heroku에서 백엔드를 호스팅하는 것입니다.


다음은 프로그래밍 경험을 향상 시키기 위해 일반적으로 사용하는 도구 목록입니다.


➡️ OS: MacOS

➡️ Terminal: iterm2

➡️ IDE:VSCode

➡️ Versioning: Git

➡️ Package Manager: NPM

➡️ Project Organization: Notion


Wireframe & Design ? 


솔직히 말해서 제품의 UI를 디자인하는 것이 별로 즐겁지 않습니다. 그래서 저는 기존 와이어 프레임으로 작업하고 대신 코드에 집중하기로 결정했습니다.


이미 말했 듯이 devchallenges에서 영감을 얻었습니다. 빠른 개요:


Alt Text 


데이터 모델링 및 API 라우팅 ? 


데이터베이스 설계 및 API 라우팅은 중요한 단계입니다. 코딩을 시작하기 전에 실행 계획이 있는지 확인하십시오. 그렇지 않으면 재앙이 될 것입니다 ?


다음은 Lucidchart로 만든 간단한 데이터 모델입니다.


Alt Text 


정말 간단하지만 이 프로젝트에는 충분합니다.


짐작할 수 있듯이, 우리는 HTTP 요청을 포함하는 Node / Express로 REST API를 구축하고 있습니다.


우리의 경로를 상상해 봅시다.


Alt Text 

Alt Text 


참고 : Apiary로 만든 API 문서


프로젝트 조직 ?️ 


나는 모든 것이 깨끗하고 잘 정리 된 것을 좋아합니다. 작업하기로 결정한 폴더 구조는 다음과 같습니다.


Alt Text 


간단하고 깨끗하며 일관된 ?


진행 상황을 추적하기 위해 Trello에서 작업 게시판을 만들었습니다.


Alt Text 


다음 단계로 넘어 가기 전에 Git 워크 플로에 대해 간략하게 설명하겠습니다.


내가 이 프로젝트에서 일하는 유일한 사람 이었기 때문에 GitHub flow은 잘 작동했습니다.


코드에 추가 할 때마다 전용 브랜치가 있으며 새 PR마다 코드를 검토합니다 (자신 만 ...).


Alt Text 


Sprint 01 : 설정 및 프런트 엔드 ? 


코딩을 시작하는 것은 항상 매우 흥미롭습니다. 이것이 제가 프로세스에서 가장 좋아하는 부분입니다.


첫 주가 가장 쉬웠다고 말하고 싶습니다. 저는 Frontend와 Backend를 설정하는 것으로 시작했습니다. 즉, 설치 종속성, 환경 변수, CSS 재설정, 데이터베이스 생성 등을 의미합니다.


설정이 완료되면 화면에 표시되어야 하는 모든 구성 요소를 구축하고 모바일 친화적인지 확인했습니다 (유연성, 미디어 쿼리 등).


다음은 구성 요소와 UI에 대한 간단한 예입니다.


// TopBar/index.tsx
import React from 'react';
import { IconButton } from '@material-ui/core';
import MenuIcon from '@material-ui/icons/Menu';

// Local Imports
import styles from './styles.module.scss';

type Props = {
  title?: String;
  menuClick: () => void;
};

const TopBar: React.FC<Props> = props => {
  return (
    <div className={styles.container}>
      <div className={styles.wrapper}>
        <IconButton className={styles.iconButton} onClick={props.menuClick}>
          <MenuIcon className={styles.menu} fontSize="large" />
        </IconButton>
        <h2 className={styles.title}>{props.title}</h2>
      </div>
    </div>
  );
};

export default TopBar;


// TopBar/styles.module.scss
.container {
  width: 100%;
  height: 60px;
  box-shadow: 0px 4px 4px rgba($color: #000, $alpha: 0.2);
  display: flex;
  align-items: center;
  justify-content: center;
}

.wrapper {
  width: 95%;
  display: flex;
  align-items: center;
}

.title {
  font-size: 18px;
}

.iconButton {
  display: none !important;
  @media (max-width: 767px) {
    display: inline-block !important;
  }
}

.menu {
  color: #e0e0e0;
}


멋진 것은 아니지만 TypeScript (아직 배울 것이 많다) 및 SCSS 모듈의 기본 구현입니다.


저는 SCSS를 많이 좋아하고 관심 있는 사람을 위해 소개를 썼습니다.


또한 내가 가장 좋아하는 UI 라이브러리 인 머티리얼 UI에서 일부 구성 요소 (아이콘, 입력 등)를 가져 오는 것을 확인할 수 있습니다.


TypeScript에 대해 말하면 첫날은 정말 고통스럽고 피곤했지만 결국 개발 중에 버그를 잡는 것이 매우 쉬웠습니다.


TypeScript로 어려움을 겪고 있다면 이 게시물을 살펴 보시기 바랍니다.


저는 Redux에 익숙하지 않아서 제대로 작성하기 위해 문서를 읽는 데 시간을 투자해야 했습니다.


내가 함께 일한 또 다른 멋진 도구는 스마트하고 간단한 방법으로 양식 유효성 검사를 관리하는 Formik입니다.


Alt Text 


// Login/index.tsx

import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import { TextField, FormControlLabel, Checkbox, Snackbar, CircularProgress } from '@material-ui/core';
import MuiAlert from '@material-ui/lab/Alert';
import { useDispatch } from 'react-redux';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { useHistory } from 'react-router-dom';

// Local Imports
import logo from '../../../assets/gc-logo-symbol-nobg.png';
import CustomButton from '../../Shared/CustomButton/index';
import styles from './styles.module.scss';

type Props = {};

type SnackData = {
  open: boolean;
  message: string | null;
};

const Login: React.FC<Props> = props => {
  const dispatch = useDispatch();
  const history = useHistory();

  const [isLoading, setIsLoading] = useState(false);
  const [checked, setChecked] = useState(false);
  const [snack, setSnack] = useState<SnackData>({ open: false, message: null });

  // Async Requests
  const loginSubmit = async (checked: boolean, email: string, password: string) => {
    setIsLoading(true);
    let response;
    try {
      response = await axios.post(`${process.env.REACT_APP_SERVER_URL}/users/login`, {
        checked,
        email: email.toLowerCase(),
        password: password.toLowerCase()
      });
    } catch (error) {
      console.log('[ERROR][AUTH][LOGIN]: ', error);
      setIsLoading(false);
      return;
    }
    if (!response.data.access) {
      setSnack({ open: true, message: response.data.message });
      setIsLoading(false);
      return;
    }
    if (checked) {
      localStorage.setItem('userData', JSON.stringify({ id: response.data.user.id, token: response.data.user.token }));
    }
    dispatch({ type: 'LOGIN', payload: { ...response.data.user } });
    history.push('');
    setIsLoading(false);
  };

  const formik = useFormik({
    initialValues: {
      email: '',
      password: ''
    },
    validationSchema: Yup.object({
      email: Yup.string().email('Invalid email address').required('Required'),
      password: Yup.string()
        .min(6, 'Must be 6 characters at least')
        .required('Required')
        .max(20, 'Can not exceed 20 characters')
    }),
    onSubmit: values => loginSubmit(checked, values.email, values.password)
  });

  return (
    <div className={styles.container}>
      <Link to="/">
        <img className={styles.logo} alt="logo" src={logo} />
      </Link>
      <form className={styles.form}>
        <TextField
          className={styles.input}
          id="email"
          label="Email"
          variant="outlined"
          type="text"
          helperText={formik.touched.email && formik.errors.email}
          error={formik.touched.email && !!formik.errors.email}
          {...formik.getFieldProps('email')}
        />
        <TextField
          className={styles.input}
          id="password"
          label="Password"
          variant="outlined"
          type="password"
          {...formik.getFieldProps('password')}
          helperText={formik.touched.password && formik.errors.password}
          error={formik.touched.password && !!formik.errors.password}
        />
        <FormControlLabel
          className={styles.check}
          control={
            <Checkbox checked={checked} onChange={() => setChecked(prev => !prev)} name="checked" color="primary" />
          }
          label="Remember me"
        />
        <CustomButton type="submit" onClick={formik.handleSubmit} isPurple title="Login" small={false} />
      </form>
      <Link to="/signup">
        <p className={styles.guest}>Don't have an account? Sign Up</p>
      </Link>
      {isLoading && <CircularProgress />}
      <Snackbar open={snack.open} onClose={() => setSnack({ open: false, message: null })} autoHideDuration={5000}>
        <MuiAlert variant="filled" onClose={() => setSnack({ open: false, message: null })} severity="error">
          {snack.message}
        </MuiAlert>
      </Snackbar>
    </div>
  );
};

export default Login;


Sprint 02 : 백엔드 ? 


서버는 매우 간단하며 Node / Express 서버가 어떻게 생겼는지에 대한 고전적인 표현입니다.


나는 몽구스 모델과 그 연관성을 만들었습니다.


그런 다음 경로를 등록하고 해당 컨트롤러를 연결했습니다. 내 컨트롤러 내에서 고전적인 CRUD 작업과 일부 사용자 지정 기능을 찾을 수 있습니다.


JWT 덕분에 보안 관련 작업이 가능해 졌는데, 이는 저에게 중요한 포인트였습니다.


이제 이 앱의 가장 멋진 기능, 양방향 통신 또는 socket.io라고 말해야 할까요?


다음은 그 예입니다.


Alt Text 


// app.js - Server side

// Establish a connection
io.on('connection', socket => {
  // New user
  socket.on('new user', uid => {
    userList.push(new User(uid, socket.id));
  });

  // Join group
  socket.on('join group', (uid, gid) => {
    for (let i = 0; i < userList.length; i++) {
      if (socket.id === userList[i].sid) userList[i].gid = gid;
    }
  });

  // New group
  socket.on('create group', (uid, title) => {
    io.emit('fetch group');
  });

  // New message
  socket.on('message', (uid, gid) => {
    for (const user of userList) {
      if (gid === user.gid) io.to(user.sid).emit('fetch messages', gid);
    }
  });

  // Close connection
  socket.on('disconnect', () => {
    for (let i = 0; i < userList.length; i++) {
      if (socket.id === userList[i].sid) userList.splice(i, 1);
    }
  });
});

// AppView/index.tsx - Client side

  useEffect(() => {
    const socket = socketIOClient(process.env.REACT_APP_SOCKET_URL!, { transports: ['websocket'] });
    socket.emit('new user', userData.id);
    socket.on('fetch messages', (id: string) => fetchMessages(id));
    socket.on('fetch group', fetchGroups);
    setSocket(socket);
    fetchGroups();
  }, []);



Express-validator를 발견했고 서버 측에서 입력 유효성 검사를 제공하는 데 많은 도움이 되었습니다. 의심 할 여지없이 다시 사용할 라이브러리입니다.


Sprint 03 : 수정 및 배포 ☁️ 


알겠습니다. 앱이 좋아 보이고 기능이 잘 작동합니다. 이 포트폴리오 프로젝트를 완료하고 새 프로젝트를 시작할 때입니다.


저는 클라우드 솔루션과 복잡한 CI / CD 방식의 전문가가 아니므로 무료 호스팅 서비스로 만족할 것입니다.


Heroku에는 백엔드에서 잘 작동하는 무료 솔루션이 있습니다. 내 노드 서버가 업로드 된 지 5 분 후에 독립적으로 실행되었습니다. 멋진 ?


클라이언트에 몇 가지 보안 문제가 발생했습니다. 일반적으로 GitHub를 통해 Netlify React 앱을 보낼 때 모든 것이 정상이지만 이번에는 아닙니다.


많은 친구들이 "보안상의 이유"로 인해 주어진 URL에 접속할 수 없었고 수정을 위해 도메인 이름을 구입해야 했습니다. 여기서 큰 문제는 아닙니다. 1 년에 15 유로가 비싼 것 같지는 않습니다.


마지막으로 사용자가 업로드 한 이미지는 공용 API를 통해 내 Cloudinary 계정에 저장됩니다.


결론 ✅ 


다시 한 번,이 프로젝트를 진행하면서 많은 것을 배웠습니다.


그 과정을 여러분과 공유하게 되어 기쁩니다. 여러분의 팁과 피드백을 듣고 싶습니다.


이 프로젝트는 포트폴리오 프로젝트에 지나지 않으며 뒤에 "제작"의도가 없습니다. 그러나 코드는 GitHub에서 오픈 소스이므로 원하는 대로 자유롭게 할 수 있습니다.


코드 품질, 보안, 최적화 측면에서 개선해야 할 사항이 많다는 것을 알고 있습니다. 어쨌든 이 작업을 마칠 수 있었고 결과가 꽤 멋져 보였고 여러분도 좋아 하셨기를 바랍니다.


라이브 버전 : GroupChat 


자신에게 도전하는 것을 멈추지 마십시오 ?


https://dev.to/killianfrappartdev/instant-messaging-app-made-with-react-typescript-node-socket-io-27pc