Sungtt

socket.io를 통한 현재 접속자 구현하기 본문

React

socket.io를 통한 현재 접속자 구현하기

sungtt 2022. 12. 3. 19:08

우선 프론트와 백에서 쓰이는 각각의 socket.io를 설치하였다.

프론트에서는 npm i socket.io-client

백에서는 npm i socket.io

 

node 설정

처음에는 4000포트를 소켓으로 사용하려했지만, https와 연동함에 있어서

그냥 서버와 같은 포트로 열도록 구현했다.

import express, { Request, Response, NextFunction } from 'express';
import { Server } from 'socket.io';
import http from 'http';

const SERVER_PORT = 3002;
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: '*',
    credentials: true,
  },
});

server.listen(SERVER_PORT, () => {
  console.log(`
    🛡️  Server listening on port: ${SERVER_PORT}
  `);
});

이어서 유저리스트가 들어있는 전역변수를 선언하였다.

유저리스트는 각각 아래의 상황에서 emit한다.

1. 사이트 접속 시

2. 로그인 시

3. 로그아웃 시

4. 소켓 연결 종료 시

 

연결이 종료되거나 로그아웃됐을 때 유저리스트에서 삭제하기위해

접속 시 고유한 socketId를 저장시켜두고, disconnect가 호출되었을 때

userList에서 해당 socketId를 찾아서 배열에서 제거한 뒤

다시 연결되어있는 소켓들에게 userList를 뿌려준다.

 

이때 App.tsx가 서버가 내려준 emit을 감지하고

받아온 userList를 전역상태값의 userList에 할당하는 디스패치를 실행한다.

socket.on('users.count', ({ id }) => {
    console.log('들어온 데이터');
    console.log(id);
    console.log(socketId);

    if (id === '첫접속') {
      io.emit('users.count', userList);
    } else if (id === '로그아웃') {
      //로그아웃시에도 해당 소켓을 삭제
      let idx = userList.findIndex((i: any) => {
        return i.socketId === socketId;
      });
      if (idx === -1) {
        io.emit('users.count', userList);
      } else {
        userList.splice(idx, 1);
        io.emit('users.count', userList);
        console.log('연결끊겼을때 유저리스트');
        console.log(userList);
      }
    } else {
      userList.push({ id, socketId: socketId });
      console.log('에밋전에 가공한 유저리스트 데이터');
      console.log(userList);
      io.emit('users.count', userList);
    }
  });

  socket.on('disconnect', function () {
    //소켓이 연결이 끊길 시 고유 소켓값과 유저수를 줄인다.
    let idx = userList.findIndex((i: any) => {
      return i.socketId === socketId;
    });
    console.log('파인드인덱스로 찾은 번호');
    console.log(idx);
    if (idx === -1) {
      io.emit('users.count', userList);
    } else {
      userList.splice(idx, 1);
      io.emit('users.count', userList);
      console.log('연결끊겼을때 유저리스트');
      console.log(userList);
    }
  });
});

 

nginx 설정

현재 sungtt.com으로 접속 시 일반 요청 경로(https://sungtt.com/)는 정적인 파일(index.html)을 제공하기때문에

81포트로 proxy_pass하고있다. 서버 요청 경로(https://sungtt.com/api/*)는 서버에 대한 요청을 처리하기때문에

서버의 포트 3002로 proxy_pass 되고있다.

이어서 개발자도구 네트워크탭에서 socketio의 요청URL을 보면 아래와 같다. 

https://sungtt.com/socket.io/?EIO=4&transport=polling&t=OJNM5yt

/socket.io라는 경로는 nginx에서 따로 설정해준것이 없기때문에 81포트로 패스되어 index.html을 반환하고있었다.

다행히도 관련된 문서 또 한 공식화 되어있었다.

https://socket.io/docs/v3/reverse-proxy/

 

Behind a reverse proxy | Socket.IO

You will find below the configuration needed for deploying a Socket.IO server behind a reverse-proxy solution, such as:

socket.io

 

요청에 대해 새로운 location 블럭을 생성해주면 된다. 아래는 공식사이트의 예제다.

http {
  server {
    listen 80;
    root /var/www/html;

    location /socket.io/ {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $host;

      proxy_pass http://localhost:3000;

      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
    }
  }
}

 

내 프로젝트 기준으로 변경했을 때는 아래 코드와 같다.

socket.io로 오는 경로들은 전부 서버로 패스시킨다. 이제 기존에 81포트로 가던 문제는 해결되었다.

 

React 설정

프론트에선 index 다음의 상위경로인 App.tsx에서 소켓을 연결하였다.

확인해보니 정상적으로 소켓연결이 된다. 이제 서버에서 'users.count'로 emit 하는 데이터를 수신할 수 있다.

  useEffect(() => {
    console.log('App 컴포넌트 useEffect');
    //소켓 이벤트마다 현재 접속자를 가져와준다..
    socket.on('users.count', function (payload) {
      console.log('소켓 온!!');
      console.log(payload);
      dispatch(connectedUser(payload));
    });
  }, []);

 

리덕스 툴킷에서는...

위에 적은 4가지 경우가 서버에서 접속자 리스트를 받아와 스토어에 할당하여,

사용자의 화면에 렌더링하기까지의 단계가 필요한 경우다.

아래 리듀서 중간중간 emit이 보이는데

사용자가 id:"첫접속"을 emit할 경우 서버에서는 현재 userList를 응답하여 현재 접속한 사람을 확인할 수 있게 했고

사용자가 id:"로그아웃"을 emit할 경우 서버에서는 해당 socketId를 찾아서 userList에서 삭제 후 응답하여 갱신시켰다.

const user = createSlice({
  name: 'user',
  initialState,
  reducers: {
    logout(state: UserStateInterface) {
      state.isLogin = false;
      state.id = '';
      state.nickname = '';
      cookies.remove('AT');
      cookies.remove('RT');
      cookies.remove('id');
      cookies.remove('nickname');
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(UserService.logoutUser.fulfilled, (state, actions) => {
        console.log('로그아웃 시도');
        state.isLogin = actions.payload.isLogin;
        state.id = actions.payload.id;
        state.nickname = actions.payload.nickname;
        //로그아웃 시 아이디를 담아 전송
        socket.emit('users.count', { id: '로그아웃' }, (res: any) => {
          console.log('로그인 에밋 후');
        });
      })
      .addCase(UserService.getUser.pending, (state, actions) => {
        console.log('getUser가 펜딩중');
      })
      .addCase(UserService.getUser.fulfilled, (state, actions) => {
        //로그인 성공 시
        //actions의 인자
        // meta : 요청에 담아 보낸 데이터, 요청아이디, 요청상태
        // payload : 요청결과로 받아온 응답데이터
        console.log('getUser가 풀필드');
        state.isLogin = actions.payload.isLogin;
        state.id = actions.payload.id;
        state.nickname = actions.payload.nickname;
        //엑시오스 헤더에 AT를 담아둔다.
        //엑시오스 헤더에 id도 담아둔다?
        console.log(actions.payload.id);
        console.log(document.cookie);
        //로그인 시 소켓에 아이디를 담아 전송
        socket.emit('users.count', { id: state.id }, (res: any) => {
          console.log('로그인 에밋 후');
        });
      })

      .addCase(UserService.getUser.rejected, (state) => {
        console.log('getUser가 리젝티드');
      })
      .addCase(UserServiceAutoLogin.getUserAutoLogin.pending, (state, actions) => {
        console.log('자동로그인 시도');
      })
      .addCase(UserServiceAutoLogin.getUserAutoLogin.fulfilled, (state, actions) => {
        //자동 로그인 성공 시
        console.log('자동로그인 완료');
        console.log(actions);
        console.log(actions.payload);
        state.isLogin = actions.payload.isLogin;
        state.id = actions.payload.id;
        state.nickname = actions.payload.nickname;
        //로그인 시 소켓에 아이디를 담아 전송
        socket.emit('users.count', { id: state.id }, (res: any) => {
          console.log('로그인 에밋 후');
          // dispatch(connectedUser([{ id, socketId: socket.id }]));
        });
      })
      .addCase(UserServiceAutoLogin.getUserAutoLogin.rejected, (state, actions) => {
        console.log('자동로그인 실패');
        //접속 시 소켓에 아이디를 담아 전송
        socket.emit('users.count', { id: '첫접속' }, (res: any) => {
          console.log('로그인 에밋 후');
          // dispatch(connectedUser([{ id, socketId: socket.id }]));
        });
      });
  },
});

 

Comments