페블_
반짝이는 시냅스
페블_
전체 방문자
오늘
어제
  • 전체글 보기 (96)
    • QA (0)
    • 프로젝트 회고 (4)
    • 프로젝트 과정 기록 (12)
    • UI 구현 연구일지 (8)
    • Front-end (31)
      • Javascript (7)
      • CSS (10)
      • React (5)
      • Typescript (3)
      • Nextjs (3)
      • 스타일링 라이브러리 (3)
    • Back-end (0)
      • Express (0)
      • DB (0)
    • CS (0)
      • 자료구조 & 알고리즘 (0)
    • CI&CD (1)
    • 툴 사용법 (4)
      • Git (1)
      • Library&패키지 (2)
      • 기타 개발관련 (1)
    • 알고리즘 이론 & 풀이 (36)
      • 백준(BOJ) (14)
      • 프로그래머스 (22)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • JS
  • react
  • 개발블로그_시작
  • 토이프로젝트
  • Python
  • 시리즈_표지
  • chartjs
  • storybook
  • 캐러셀
  • UI 컴포넌트
  • 알고리즘
  • TypeScript
  • 생각
  • 선형대수학
  • 백준
  • eslint
  • 파이썬
  • emotion

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
페블_

반짝이는 시냅스

[Project] Youtube Party 만들기 (2) - Express 디렉토리 구조 설정, 채팅 room 생성 API 만들기
프로젝트 과정 기록

[Project] Youtube Party 만들기 (2) - Express 디렉토리 구조 설정, 채팅 room 생성 API 만들기

2023. 10. 6. 22:03

채팅인데 방 생성 API를 만드는 이유

나는 기존에 튜토리얼에서 흔히 하듯이 유저가 직접 방 이름을 입력해 socket 채팅방에 참여하는 방식이 아니라, 서버에 의해 방 이름을 받아 접속하도록 하고 싶었다. 그래서 방 POST, GET API를 만들었는데, 그것을 이용해 채팅방을 생성하는 과정은 다음과 같다.

  1. Nextjs에서 유저가 방 생성 버튼을 클릭하면 API에 방 생성 요청을 보낸다.
  2. 서버는 랜덤 문자열을 생성하고, 그 문자열을 roomId로 하는 방 객체를 만든다. 그리고 클라이언트에게 roomId를 전달한다.
  3. 클라이언트는 받아온 roomId에 해당하는 페이지로 이동한다. ex) localhost:3000/party/dflc24dfsd
  4. 처음 roomId에 해당하는 페이지로 이동해 connect될 때 joinRoom 이벤트를 보내고, 서버는 유저를 roomId 방에 join시켜준다.

여기까지가 처음 방을 생성해 입장하는 유저의 경우이고,

만들어진 방에 참여하는 경우에는 방 페이지의 url을 주소창 등에 붙여넣으면 된다.

 

클라이언트가 방에 입장할 때는 방이 존재하는지 API에 요청을 보내 검사한 다음, 존재하지 않는다는 응답이 돌아오면 404 페이지를 보여로 리다이렉트 하게 했다. Nextjs를 사용하고 있기 때문에 페이지에 진입하기 이전에 serverSideProps를 이용해 조건에 따라 리다이렉트하는 것을 구현하기 쉬웠다.

 

방 API를 만드는 것의 이유는 POST 메소드의 경우 방 생성을 서버에게 일임하기 위함이고, GET의 경우에는 방의 존재여부를 검사해서, 유저가 방을 직접 생성할 때 오타로 인해 엉뚱한 방에서 기다릴 수도 있는 상황을 방지할 수 있다는 것이다.

 

서버 디렉토리 구조

.
└── Project/
    ├── server/
    │   └── src/
    │       ├── bin/
    │       │   └── www.ts
    │       ├── lib/
    │       │   └── socket.ts
    │       ├── router/
    │       │   └── rooms.ts
    │       ├── controller/
    │       │   └── rooms.ctrl.ts
    │       ├── service/
    │       │   └── rooms.ts
    │       │   └── messages.ts
    │       │   └── users.ts
    │       ├── types/
    │       │   └── message.d.ts
    │       ├── app.ts
    │       ├── package.json
    │       ├── package-lock.json
    │       ├── .env
    │       └── tsconfig.json
    └── client

 

bin/www.ts 는 서버를 구동하는 주체, app.ts는 서버 최상위, router -> controller -> service 순으로 로직의 세분화가 이뤄지고 socket 같은 큰 로직은 모듈로 분리해 lib에 위치시켰다.

type 폴더는 service 파일에 작성하기에 타입이 길어진다 싶으면 타입 부분만 따로 분리한 파일들을 넣는 곳이다.

 

bin/www.ts

서버를 구동시키는 코드를 작성한다. app.ts를 불러와서 listen 하거나 socket 서버 listen, 포트 설정, 서버 실행 문구를 console에 출력하는 등의 동작을 수행한다.

const PORT = process.env.PORT || '5000';

import server from '../app';

server.listen(PORT, () => {
  console.log(`Server is running on port${PORT}`);
});

 

app.ts (라우터 등록)

서버 객체라고 할 수 있는 파일.

주로 cors 등의 미들웨어를 설정하고 라우터를 설정한다.

 

소켓 통신처럼 긴 로직은 여기에 바로 작성하지 않고 모듈로 만든 다음, import해서 사용할 수 있도록 만드는게 좋다.
express나 cors를 import한 다음 express(), cors() 처럼 실행해주는 것을 생각해보면 된다.

 

import express from 'express';
import http from 'http';
import path from 'path';
import cors from 'cors';
import { router as rooms } from './router/rooms';
import { initSocket } from './lib/socket';

const app = express();
const server = http.createServer(app);

app.use(cors());

app.use('/api/rooms', rooms);

export default server;

 

+) router에서 url 앞에 /api를 붙이는 이유

더보기

API router의 경로는 '/api/리소스명' 형태로 네이밍 하는게 좋다. ex) /api/rooms
서버와 클라이언트를 localhost:5000, localhost:3000에서 따로따로 실행하는 것처럼, client 와 API 서버를 따로 운영한다면 '/api'를 안 붙여도 큰 문제는 발생하지 않을 수 있지만, 되도록 권장한다.

 

React를 빌드한 index.html을 서버가 보내는 방식으로 배포했다면 경험해봤겠지만, 이렇게만 작성하면 루트(/) 경로 외의 페이지로 이동하려고 할 때 not Found 에러가 발생한다.

app.get('/', function (req, res) {
  res.sendFile(path.join(__dirname, '/../index.html'));
});

 

왜냐하면 이 상황에서 브라우저 url 창에 경로를 입력하는 것은 서버에게 GET 요청을 보내는 것과 같기 때문이다.

 

브라우저에 /products를 입력하면 서버는 /products 경로에 대해 요청을 받는다.

/products가 만약 html이 아니라 API을 서비스하는 경로였다면 의도치 않게 API를 호출하는 셈이 된다.

 

그렇기 때문에 이 둘을 구별하기 위해 앞에 API 경로 앞에는 /api를 붙이는게 권장되는 것이다.

// html 파일 보내주는 경우
app.get('/products', function (req, res) {
  res.sendFile(path.join(__dirname, '/../public/index.html'));
});

// API
app.get('/products', productsRouter);

 

이렇게 작성하자

// html
app.get('/products', function (req, res) {
  res.sendFile(path.join(__dirname, '/../public/index.html'));
});

// API
app.get('/api/products', productsRouter);

 

 

 

router/rooms.ts

router 이름은 createRoom, deleteRoom 이런 식으로 말고, rooms 처럼 리소스 단위로 만드는게 좋다.

create, delete같은 동작은 이름이 아니라 get, post 메소드로 나타내면 되기 때문이다.

 

router는 경로를 매핑하는 역할만 하는게 좋기 때문에, 자세한 로직은 controller가 담당하게 한다.

/* router/rooms.ts */
import express from 'express';
const router = express.Router();
import * as ctrl from '../controller/rooms.ctrl';

router.get('/:roomId', ctrl.getRoomDataController);
router.post('/', ctrl.createRoomController);

export { router };

 

유저가 접속할 때 roomId에 해당하는 방이 존재하는지 여부를 확인하기 위한 API 하나,

방 생성 API 하나 이렇게 만들었다.

 

controller/rooms.ctrl.ts

controller 파일에 담아도 충분하지만, controller라는 것을 쉽게 구분할 수 있도록 rooms.ctrl.ts처럼 파일명에 .ctrl을 붙여서 생성해주자.

 

controller는 세부적인 로직보다 값의 입출력을 담당한다는 느낌으로 만들면 된다. 

방 객체 관리나 createRoom, getRoom 같은 세세한 로직은 service로 분리했고, controller에서는 데이터의 입출력과 큼직한 로직을 쉽게  알 수 있도록 작성했다.

/* controller/rooms.ctrl.ts */
import { createRoom, getRoom } from '../service/rooms';

// controllers
export const getRoomDataController = (req, res) => {
  const { roomId } = req.params;
  const room = getRoom(roomId);
  if (room) {
    const { timeoutId, ...roomData } = room;
    res.json(roomData);
  } else {
    res.status(404).end();
  }
};

export const createRoomController = (req, res) => {
  const roomId = Math.random().toString(36).substring(2, 8);
  createRoom(roomId);
  res.status(201).json({ roomId });
};

 

service/rooms.ts

방에 유저 추가, 제거, 방 삭제 등에 관한 로직을 담은 Room 클래스와,

Room들의 집합인 rooms 객체, rooms와 관련된 함수가 존재한다.

 

여기에 User, Message 등의 객체도 등장하는데 이건 다음에 작성할 채팅 보내기 편에서 설명할 것이다.

필요하다면 소스코드 참조

 

export const rooms: Record<string, Room> = {}; 라는 rooms 객체를 이용해 방 목록을 관리할 것이다.

타입을 보면 알겠지만 rooms는 roomId와 Room 인스턴스의 짝을 담은 객체이다.

 

처음에는 addUser같은 메소드도 모두 createRoom 함수와 마찬가지로 function으로 만들었었는데, 단수의 Room이라는 객체에 대한 로직들은 하나로 모으면 좋을 것 같아서 class 형태로 리팩토링했다.

 

이렇게 만드는 것의 장점은 방을 삭제하고 싶을 때, 함수 형태였다면 deleteRoom(roomId)처럼 방 아이디를 알아야 했겠지만, room.deleteRoom()처럼 자신을 지우도록 메소드를 만들면 편하다는 것이다.

 

createRoom 함수도 Room.createRoom처럼 static 메소드로 집어넣으면 응집도가 커지긴 하겠지만, js에서 철저하게 객체지향적으로 만들고 싶지 않아서 그냥 놔뒀다. 난 Room과 Room들의 목록인 rooms라는 객체는 별개의 것이라고 생각하는 편이다.

import { User } from './users';

class Room {
  roomId: string;
  users: User[];
  timeoutId: NodeJS.Timeout | null;

  constructor(roomId: string) {
    this.roomId = roomId;
    this.users = [];
    this.timeoutId = null;

    // 방 생성하고 2분안에 아무 유저도 들어오지 않는다면 방 삭제.
    // 네트워크 오류로 인해 방 생성만 되고 유저가 아무도 들어오지 않았을 때 정크 데이터가
    // 남지 않게 하기 위해서다.
    this.timeoutId = setTimeout(() => {
      if (rooms[roomId].users.length === 0) {
        this.deleteRoom();
      }
    }, 2 * 60 * 1000); // 2분
  }

  addUser(user: User) {
    this.users.push(user);
    // 방 자동삭제 타이머 해제
    clearTimeout(this.timeoutId);
    delete this.timeoutId;
  }

  getUsers() {
    return this.users;
  }

  // 유저가 0명 남았을 때 방 삭제
  // 유저가 나가서 방 인원이 0명이 되었어도 일정 시간 안에 다시 돌아온다면
  // 위의 addUser 메소드의 clearTimeout에 의해 방 삭제는 취소된다.
  removeUser(socketId: string) {
    this.users = this.users.filter((user) => user.socketId !== socketId);
    if (this.users.length === 0) {
      this.timeoutId = setTimeout(() => {
        if (rooms[this.roomId].users.length === 0) {
          this.deleteRoom();
        }
      }, 10 * 60 * 1000);
    }
  }

  deleteRoom() {
    delete rooms[this.roomId];
  }
}

export const rooms: Record<string, Room> = {};

export function createRoom(roomId: string) {
  rooms[roomId] = new Room(roomId);
}

export function getRoom(roomId: string) {
  const room = rooms[roomId];
  if (rooms[roomId]) {
    return room;
  } else {
    return null;
  }
}

 

'프로젝트 과정 기록' 카테고리의 다른 글

[Project] Youtube Party 만들기 (4) - socket 모듈 만들기 (Nextjs, Express로 채팅 기능 구현)  (0) 2023.10.23
[Project] Youtube Party 만들기 (3) - Nextjs 채팅방 페이지로 이동하는 기능 만들기 +) getServerSideProps 리다이렉트  (1) 2023.10.17
[Project] Youtube Party 만들기 (1) - 프로젝트 환경설정 | Nextjs + Typescript + Express  (0) 2023.09.23
[Project] Youtube Party 만들기 - 프로젝트 주제, 컨셉  (0) 2023.09.22
[React] Chartjs로 막대그래프 그리기  (0) 2023.08.04
    '프로젝트 과정 기록' 카테고리의 다른 글
    • [Project] Youtube Party 만들기 (4) - socket 모듈 만들기 (Nextjs, Express로 채팅 기능 구현)
    • [Project] Youtube Party 만들기 (3) - Nextjs 채팅방 페이지로 이동하는 기능 만들기 +) getServerSideProps 리다이렉트
    • [Project] Youtube Party 만들기 (1) - 프로젝트 환경설정 | Nextjs + Typescript + Express
    • [Project] Youtube Party 만들기 - 프로젝트 주제, 컨셉
    페블_
    페블_

    티스토리툴바