[socket.io] React, redux-saga eventChannel에 socket.io 사용하기 - (redux-saga에 socket.io 연결)

권준혁·2021년 5월 30일
0

socket

목록 보기
1/1
post-thumbnail

socketIO를 이용해 양방향 통신을 구현하는 것은 꽤 간단하다. (물론, 인증이나 채팅룸 등 다양한 기능을 추가하면 복잡해진다.)
하지만, 리액트를 이용해 개발을 하다보면 redux-saga를 자연스럽게 자주 쓰게되는데, 생각보다 까다로웠다. 리덕스 사가의 채널을 이용해 소켓통신을 구현했을 때 장점에 대해 짚어보고, 양방향 통신을 직접 구현하며 포스팅을 진행하려한다.

SocketIO

  • socketIO는 실시간 웹 애플리케이션을 위한 Javascript라이브러리다.
  • 웹 클라이언트와 서버간에 실시간 양방향 통신이 가능하다.
  • 채팅 등의 기능을 구현하는데 적절하다.
  • 클라이언트 측 라이브러리, 서버(Node) 측 라이브러리 두 부분으로 구성된다.

먼저 redux 설계는 ducks 패턴을 이용했다.
포스팅 주제와는 다르니 제외하고 소켓과 리덕스사가 채널을 연결하는 부분만 살펴본다.
소스코드는 포스팅 하단에 적어놓겠다.

우선 서버측 코드부터 작성해본다.
전체 코드 양도 많지않지만, 서버측이 20% 클라이언트가 80% 정도 되는 것 같다.
서버 코드는
express 공식문서의 hello world 예제를 프로젝트 루트의 server.js에 복붙하고, socket 코드만 추가했다.


1. 서버 (express 노드서버)

먼저 expresssocket.io를 설치한다.

yarn add socket.io express

프로젝트 구조

간단하게 server.jssocket.js로만 구성돼있다.

1. server.js

>> express 공식문서

const express = require('express')
const app = express()
const port = 3000

// http서버
const server = require('http').createServer(app)
const socket = require("./socket")

// 소켓설정 부분
socket(server, app)

// GET /some으로 요청이 들어올 경우 소켓접속자에게 데이터를 전송
app.get("/some", (req,res) => {
  console.log("------------")
  const io = app.get("io")
  io.emit("tasks", { id: 9999})
  res.send("Hi")
});

server.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

2. socket.js

>> socket.io server api

const SocketIO = require("socket.io");

module.exports = (server, app) => {
    const io = SocketIO(server, {
        cors:{
            origin: "*",
            methods: ["*"]
        },
        path: "/socket"
    });
    app.set("io", io);
    
    io.on("connect", (socket) => {
        console.log("----- socket connected");
      
        let id = 0;
        setInterval(function() {
            io.emit("tasks", { id: id, images: testImages});
            id++;
            if (id >= 20) {
                clearInterval(this);
            }
        }, 1500);

        socket.on("disconnect", () => {
            console.log("----- socket disconnected");
        })
    })
}

socket.js는 함수를 module.exports하고 있는데, 내보내는 함수의 내부에서 socket객체를 생성한 뒤, app.set("io", io)로 express 구성에 추가했다. 여기서 추가해줬기 때문에, GET /some 요청에 socket을 사용할 수 있다. 앱 내에서 언제든 소켓객체를 사용할 수 있다.
특정 미들웨어를 만들어, 특정 라우트에서만 소켓객체를 사용할 수도 있겠다.
io.on("connect", (socket) => {}) 부분을 보면 소켓에 연결되었을 때 어떤 동작을 수행할 지 작성 되어있다. connectconnection은 동일하다.
이 함수 내부의 코드를 보면 setInterval함수로 1.5초 마다 emit을 수행 하고 있다. 연결된 클라이언트에게 특정 데이터를 전달하는 역할을 한다. 보다시피 1.5초마다 데이터를 전송하고 있고 id값이 증가하며, id가 20이상이되면 clearInterval로 해제한다.
그리고, connect disconnect 일 때는 서버측에서 콘솔로그만 찍는다.

서버측 코드 작성이 끝났다.


2. 클라이언트 (React + Redux-saga eventChannel + socket.io-client)

1. 로직

앱 최초실행 즉시 소켓으로 서버에 연결한다.
연결 후 서버에서는 1.5초마다 소켓을 통해 데이터를 전달하고 20회 반복한다.
추가로 GET /some 라우트로 요청이 들어오면 소켓으로 응답한다.

2. 모듈 설치

create-react-app를 이용해 간단하게 프로젝트를 생성하고 필요한 모듈들을 설치한다
create-react-app과 redux, redux-saga에 대한 설명과 코드는 포스팅내용에서 생략했다.
하단에 클라이언트측의 소스코드 링크가 있으니 참고하면 좋다.

create-react-app socket-test-client && cd socket-test-client

다음으로 리덕스, 사가, 소켓클라이언트를 포함해 필요한 모듈을 설치한다.

yarn add socket.io-client redux react-redux redux-saga immer

3. 코드

3-1. 소켓 인스턴스 모듈화

socket.js라는 파일을 만들고 socket객체를 내보내도록 만들어보자

import io from 'socket.io-client';

// 소켓 연결 객체
export default io("http://localhost:3000", {path: "/socket"});

이제 이 소켓 객체를 import해서 사용하면 된다.

3-2. 앱 마운트 시 소켓연결 dispatch 실행, redux구축
// 소켓데이터 수신하는 채널 "tasks" 생성
useEffect(() => {
  dispatch(socketActions.waitTask());
}, []);

socketContainer.js라는 이름의 컴포넌트를 만들고 최초 마운트 시 소켓연결 action을 dispatch하고 있다.

이 액션함수 waitTask가 dispatch되면 사가함수가 실행되도록 코드를 작성한다.
리덕스는 ducks패턴으로 작성했다.

먼저 redux의 reducer생성 함수를 만든다. immer 라이브러리를 이용했다

// createReducer.js

import produce, {enableES5} from 'immer';
// createReducer는 reducer 생성자 함수
// reducer를 리턴한다.
// reducer는 immer패키지의 produce메서드를 이용해서
// action에 따라 store의 상태를 불변객체로 변경한다.
export default function createReducer(initialState, handlerMap) {
  enableES5();
  return function (state = initialState, action) {
    return produce(state, (draft) => {
      const handler = handlerMap[action.type];
      if (handler) {
        handler(draft, action);
      }
    });
  };
}

이어서 ducks패턴으로 리덕스를 구축한다.
store의 상탯값을 직접 조작하는 부분은 아직 없기 때문에 types actions에만 waittask를 추가했다.

// state.js
import createReducer from '@utils/createReducer';

export const types = {
  WAIT_TASK: 'socket/WAIT_TASK',
};

export const actions = {
  waitTask: () => ({
    type: types.WAIT_TASK,
  }),
};
export const INITIAL_STATE = {
  tasks: [],
};

const reducer = createReducer(INITIAL_STATE, {
});

export default reducer;
3-3. saga 함수 작성
// saga.js

import { call, put, take, takeEvery } from 'redux-saga/effects';
import socket from '~/config/socket';
import { closeChannel, createSocketChannel } from './createSocketChannel';
import { actions, types } from './state';

  function* waitTask() {
    let channel;
    try {
      channel = yield call(createSocketChannel, "tasks");
      while(true) {
        const task = yield take(channel);
        yield onTask(task);
      }
    } catch(e) {
      console.log(e, "error");
    } finally {
      socket.close();
      closeChannel(channel);
    }
  }
  
  function* onTask(task) {
    yield put(actions.pushTask(task));
  }

  export default function* watcher() {
    yield takeEvery(types.WAIT_TASK, waitTask);
  }

가장 아랫부분에서 보면 takeEvery로 모든 dispatch를 놓치지않고 구독하도록 했다. WAIT_TASK가 dispatch하면 waitTask사가함수가 실행된다.

사가함수 waitTask가 실행되면 createSocketChannel이라는 함수를 호출하는데 여기에 사가의 eventChannel기능을 이용해 socket을 연결했다.

// createSocketChannel.js
import {eventChannel, buffers} from 'redux-saga';
import socket from '../../config/socket';

// 기본 matcher, buffer
const defaultMatchers = () => true; 
const defalutBuffer = buffers.none();

// 소켓 이벤트채널 생성 팩토리함수
export function createSocketChannel(eventType, buffer = defalutBuffer, matchers = defaultMatchers) {
    return eventChannel(
        emit => {
            const emitter = (message) => {
                emit(message);
            };
            socket.on(eventType, emitter);
            // 항상 unsubscribe 함수를 반환해야한다.소스코드가 종료되기전에 socket.off 시키고있다
            // .
            return () => {
                socket.off(eventType, emitter);
            }
        },
        buffer,
        matchers,
    )
}

export function closeChannel(channel) {
    if (channel) {
      channel.close();
    }
}

아까 만들었던 소켓인스턴스를 사용하고 있으며, matcherbuffer는 추가적인 기능이다. 2,3번째 인자로 그 값이 주어지지 않을경우 defaultMatchersdefaultBuffer를 이용한다.

eventChannelemit을 이용해 채널을 take했을 때 반환할 값을 넣어준다.
socket.on(eventType, emitter); 이벤트 타입에 맞는 소켓이벤트가 실행됐을 때 채널은 소켓을 통해 전달된 값(message)을 리턴한다.

다시 saga.js로 돌아가서
const task = yield take(channel)에 따라 전달된 task로 onTask를 호출한다.

  function* onTask(task) {
    yield put(actions.pushTask(task));
  }

pushTask는 리덕스 스토어에 배열형태로 데이터를 추가해주는 액션이다.

// state.js
import createReducer from '@utils/createReducer';
import moment from 'moment';

export const types = {
  WAIT_TASK: 'socket/WAIT_TASK',
  PUSH_TASK: 'socket/PUSH_TASK',
};

export const actions = {
  waitTask: () => ({
    type: types.WAIT_TASK,
  }),
  pushTask: (payload) => ({
    type: types.PUSH_TASK,
    payload,
  }),
};
export const INITIAL_STATE = {
  tasks: [],
};

const reducer = createReducer(INITIAL_STATE, {
  [types.PUSH_TASK]: (state, action) => {
    state.tasks.push({...action.payload, time: moment(new Date()).format("HH시 mm분 ss초") });
  },
});

export default reducer;

앱 마운트 시 saga eventChannel을 이용한 소켓연결, 서버에서 데이터 전송 시 redux store에 데이터가 추가되는 로직이 완성됐다.

3. 화면

화면 측 코드는 생략했다.
redux store의 배열데이터를 가져와 화면에 보여줬다.
연결상태는 소켓인스턴스의 connect, disconnect 이벤트를 이용해 리액트 상태값을 변경해주면 된다.

  useEffect(() => {
    socket.on("connect", () => {
      setOn(true);
    })
    socket.on("disconnect", () => {
      setOn(false);
      socket.connect();
    })
  }, []);

GET /some으로 요청을 보내서 서버에서 socket을 이용한 응답을 하는지 확인해본다.

app.get("/some", (req,res) => {
  console.log("------------");
  const io = app.get("io");
  io.emit("tasks", { id: 9999, images: testImages});
  res.send("aa");
});

http요청에도 연결된 socket을 활용할 수 있다.

간단하게 socket.io와 사가의 eventChannel을 이용해 소켓통신을 구현해봤다.
socket.io는 단순한 양방향 통신외에도 인증이나 채널 귓속말 등 다양한 기능을 제공한다.
리덕스 사가의 channelsocket.io의 궁합이 꽤 좋은 것 같다. 이 후에 양방향통신 구현이 필요할 때에 활용할지 고민해봐야겠다.

참고글: https://meetup.toast.com/posts/114
참고글: socket.io 공식문서
참고글: redux-saga 한글 (채널)
Github 리포지토리: 클라이언트

profile
웹 프론트엔드, RN앱 개발자입니다.

0개의 댓글