OHJINSU BLOG
  1. About
  2. Blog

© 2025 오진수. All rights reserved.
이 블로그는 Makernote로 만들어졌습니다.

profile image오진수 · 프로그래밍 · 

Remix(Express Server) + Socket.IO 설정하기


이미지 출처: Przemyslaw Marczynski on Unsplash


Remix 기반 Fullstack 웹 애플리케이션 서버를 운영중이었는데, 채팅 기능을 추가할 일이 생겼습니다.


채팅 기능은 쌍방향 통신을 필요로 합니다. 쌍방향 통신을 구현하는 방법은 여러가지가 있어요. 하지만 첫째로는 생산성, 둘째로는 Flutter와 연동해야 했으므로 호환성 및 라이브러리 지원을 고려해서 Socket.IO를 선택했습니다.


Custom Server


Remix와 Socket.IO를 연동하기 위해서는 우선 Custom Server를 준비해야 합니다. Custom Server는 Remix 공식 문서가 설명하고 있어요.


Remix는 Express 위에서 실행됩니다. Custom Server는 쉽게 말해서 숨겨진 Express를 밖으로 끄집어내는 설정입니다. Express 관련 설정을 직접 조작할 수 있도록 말이죠. Socket.IO도 express 객체를 필요로 합니다.


별도로 Custom Server 설정이 없다면 npm run start 명령어를 입력했을 때 remix-serveExpress를 실행시킵니다. 구체적인 코드가 궁금한 분들은 remix-run/serve 레포지토리를 살펴보세요.


하지만 Remix 공식 문서처럼 Express를 끄집어냄과 동시에 Hot reload를 지원하는 Dev mode가 필요했습니다. 공식 문서는 vite dev server를 고려하지 않은 간단한 예제거든요.


그래서 구체적인 코드는 Remix + Express 템플릿을 찾아 참고했습니다.


좌우지간 Custom Server를 통해 Express를 끄집어내면 Socket.IO는 간단하게 추가할 수 있습니다. Socket.IO를 추가한 코드는 아래와 같습니다.


// server.js

import { createRequestHandler } ofrom "@remix-run/express";
import compression from "compression";
import express from "express";
import morgan from "morgan";
import { createServer } from "http";
import { Server } from "socket.io";

const MODE = process.env.NODE_ENV;

const PORT = process.env.PORT || 3000;

const viteDevServer =
  MODE === "production"
    ? undefined
    : await import("vite").then((vite) =>
        vite.createServer({
          server: { middlewareMode: true },
        })
      );

const app = express();

const httpServer = createServer(app);

const io = new Server(httpServer);

io.on("connection", (socket) => {
  console.log(socket.conn.remoteAddress, socket.id, "connected");

  socket.emit("event", "Hello from server");

  socket.on("something", (data) => {
    console.log(socket.id, data);

    socket.emit("pong");
  });
});

app.use(compression());

app.disable("x-powered-by");

if (viteDevServer) {
  app.use(viteDevServer.middlewares);
} else {
  app.use(
    "/assets",
    express.static("build/client/assets", { immutable: true, maxAge: "1y" })
  );
}

app.use(express.static("build/client", { maxAge: "1h" }));

app.use(morgan(MODE === "production" ? "combined" : "tiny"));

const remixHandler = createRequestHandler({
  build: viteDevServer
    ? () => viteDevServer.ssrLoadModule("virtual:remix/server-build")
    : await import("./build/server/index.js"),
  mode: MODE,
  getLoadContext() {
    return { io };
  },
});

app.all("*", remixHandler);

httpServer.listen(PORT, () =>
  console.log(`Express server listening at ${PORT}`)
);


Custom Server 때문에 진입점이 바뀌었으므로 package.json에서 시작 스크립트를 수정해 줍니다.


// package.json
{
  "scripts": {
    "dev": "node ./server.js",
    "start": "dotenv -e .env cross-env NODE_ENV=production node ./server.js",
  },
}


더 이상 remix-run/serve가 아니라, node가 직접 server.js 파일을 실행시키게 됩니다.


참고로 Remix는 dev 모드에서만 .env 파일을 자동으로 읽어 줍니다. 그래서 npm run start에서는 환경변수 관련 스크립트를 함께 작성해 주었어요.


여기까지 하면 개발 모드 덕분에 애플리케이션 코드를 수정하면 Hot reload 등 편의 기능은 잘 작동합니다. 그렇지만 server.js 파일을 수정했을 때 작동하지는 않아요.


Nodemon 등을 이용해 추가 설정을 해줄 수는 있지만 조금 더 간편한 방법도 있습니다. 바로 최대한 server.js 코드를 수정하지 않는 방법이죠.


보통 방식대로라면 클라이언트측 요청을 추가할 때마다 server.js 파일에서 처리하는 코드를 같이 추가해 주어야 할 겁니다. 하지만 클라이언트측 요청을 HTTP 프로토콜로 보내면 server.js를 수정할 일은 거의 없게 됩니다.


Socket.IO를 이용할지라도 굳이 서버측에서 요청을 Socket.IO로 받을 의무는 없어 보입니다. Socket.IO는 서버측이 클라이언트측에게 (요청 시점과 무관한 어느 때에) 메시지를 보내야 하는 경우에만 이용하면 됩니다.


예를 들어 클라이언트측으로부터 채팅 메시지를 받았을 때 서버가 해당 메시지를 다른 클라이언트측에게 보내야 하는 경우가 있습니다. 이때 클라이언트에게 채팅 메시지는 HTTP 프로토콜을 사용하는 API를 통해 받습니다. 그리고 채팅 메시지를 다시 다른 클라이언트에게 보낼 때에만 Socket.IO를 이용하는 방식입니다.


그러기 위해서는 해당하는 스코프에서 Socket.IO의 Server 객체에 접근할 수 있어야 합니다. 방금 이야기한 예에서는 API 핸들러에서 접근할 수 있어야겠죠. 앞선 예제 코드에서는 getLoadContextServer 객체를 반환하도록 만든 이유입니다.


덧붙여서, Typescript 기반 Remix Handler에서 타입 자동완성을 지원받고자 한다면 선언 파일을 프로젝트에 추가해줍니다.


// server.d.ts
import type { Server } from "socket.io";

declare module "@remix-run/node" {
  export interface AppLoadContext {
    io: Server;
  }
}


덧붙이자면, Flutter에서 Socket.IO를 연결하는 코드는 다음과 같습니다.


final option = OptionBuilder().setTransport(['websocket']).build();

final socket = io("http://localhost:3000", socket);

socket.onConnect((_) {
  print("Socket connected");
});


Transport를 websocket으로 설정하지 않으면 연결되지 않으니 주의하시길 바랍니다.

댓글 0

프로그래밍 카테고리 다른 글