이 튜토리얼에서는 웹 소켓을 사용하여 채팅 애플리케이션을 구축합니다. 웹 소켓은 실시간 데이터 전송이 필요한 애플리케이션을 구축하려는 경우 정말 유용합니다.
이 튜토리얼이 끝나면 자체 소켓 서버를 설정하고, 실시간으로 메시지를 보내고 받고, Redis에 데이터를 저장하고, 렌더링 및 Google Cloud Run에 애플리케이션을 배포할 수 있게 됩니다.
채팅 애플리케이션을 구축할 예정입니다. 간략하게 설명하기 위해 서버만 설정하겠습니다. 자신만의 프런트 엔드 프레임워크를 사용하고 따라갈 수 있습니다.
이 채팅 애플리케이션에는 방이 있으며 사용자는 방에 참여하여 채팅을 시작할 수 있습니다. 모든 것을 단순하게 유지하기 위해 사용자 이름이 고유하지 않다고 가정합니다. 그러나 각 방에는 특정 사용자 이름을 가진 사용자가 한 명만 있을 수 있습니다.
먼저 필요한 종속성을 설치해야 합니다.
npm i express cors socket.io -D @types/node
우리는 소켓 서버를 설정하기 위해 http 모듈을 사용할 것입니다. 우리 앱이 터미널에서 실행될 것이기 때문에 모든 출처를 허용해야 합니다.
import express from "express"; import cors from "cors" import { Server } from "socket.io"; import { createServer } from "http" const app = express(); const server = createServer(app); // create a socket server. const io = new Server(server, { cors: { origin: "*", credentials: true, } }); // listen to connections errors io.engine.on("connection_error", console.log) app.use(cors()) const PORT = 3000; server.listen(PORT, () => console.log(`Server running on port ${PORT}`));
우리는 Redis를 사용하여 룸 및 사용자 정보와 함께 메시지를 저장할 것입니다. upstash redis(무료)를 사용할 수 있습니다. Upstash 대시보드에서 새 Redis 인스턴스를 생성합니다. 생성 후에는 Redis 인스턴스에 연결하는 데 사용할 수 있는 Redis URL을 받게 됩니다.
원하는 Redis 클라이언트를 설치하세요. 저는 ioredis를 사용할 예정입니다.
npm i ioredis
다음으로 Redis 클라이언트를 초기화하고 얻은 연결 URL을 사용하여 Redis 서버에 연결합니다.
/** /src/index.ts */ import { Redis } from "ioredis" if (!process.env.REDIS_URL) throw new Error("REDIS_URL env variable is not set"); const redis = new Redis(process.env.REDIS_URL); // listen to connection events. redis.on("connect", () => console.log("Redis connected")) redis.on("error", console.log)
사용자는 방을 만들거나 기존 방에 참여할 수 있습니다. 객실은 고유한 객실 ID로 식별됩니다. 각 구성원은 전체가 아닌 방 내에서 고유한 사용자 이름을 갖습니다.
Redis 세트에 룸 ID를 저장하여 서버의 모든 활성 룸을 추적할 수 있습니다.
우리의 목적에 따라 사용자 이름은 방 내에서만 고유합니다. 그래서 우리는 방 ID와 함께 세트에 저장합니다. 이렇게 하면 룸 ID와 구성원 ID의 조합이 전 세계적으로 고유하게 됩니다.
방 생성을 위한 소켓 이벤트를 설정할 수 있습니다. 룸을 생성할 때 생성을 요청한 멤버도 룸에 추가합니다.
io.on("connection", () => { // ... socket.on("create:room", async (message) => { console.log("create:room", message) const doesRoomExist = await redis.sismember("rooms", message.roomId) if (doesRoomExist === 1) return socket.emit("error", { message: "Room already exist."}) const roomStatus = await redis.sadd("rooms", message.roomId) const memStatus = await redis.sadd("members", message.roomId "::" message.username) if (roomStatus === 0 || memStatus === 0) return socket.emit("error", { message: "Room creation failed." }) socket.join(message.roomId) io.sockets.in(message.roomId).emit("create:room:success", message) io.sockets.in(message.roomId).emit("add:member:success", message) }) }
기존 룸에 새 구성원을 추가하려면 먼저 해당 룸에 해당 구성원이 이미 존재하는지 확인해야 합니다.
io.on("connection", () => { // ... socket.on("add:member", async (message) => { console.log("add:member", message) const doesRoomExist = await redis.sismember("rooms", message.roomId) if (doesRoomExist === 0) return socket.emit("error", { message: "Room does not exist." }) const doesMemExist = await redis.sismember("members", message.roomId "::" message.username) if (doesMemExist === 1) return socket.emit("error", { message: "Username already exists, please choose another username." }) const memStatus = await redis.sadd("members", message.roomId "::" message.username) if (memStatus === 0) return socket.emit("error", { message: "User creation failed." }) socket.join(message.roomId) io.sockets.in(message.roomId).emit("add:member:success", message) }) socket.on("remove:member", async (message) => { console.log("remove:member", message) const doesRoomExist = await redis.sismember("rooms", message.roomId) if (doesRoomExist === 0) return socket.emit("error", { message: "Room does not exist." }) await redis.srem("members", message.roomId "::" message.username) socket.leave(message.roomId) io.sockets.in(message.roomId).emit("remove:member:success", message) }) }
마지막으로 채팅 이벤트를 생성합니다.
io.on("connection", () => { socket.on("create:chat", (message) => { console.log("create:chat", message) redis.lpush("chat::" message.roomId, message.username "::" message.message) io.sockets.in(message.roomId).emit("create:chat:success", message) }) }
소켓 서버에는 지속적인 연결이 필요하며 서버리스 환경에서는 작동하지 않습니다. 따라서 vercel에서는 소켓 서버를 배포할 수 없습니다.
Render, fly.io, Google Cloud Run 등 다양한 위치에 배포할 수 있습니다.
렌더링 시 배포가 간단합니다. dockerfile이 있으면 해당 dockerfile에서 프로젝트를 자동으로 빌드합니다. Render에는 무료 등급이 있지만 무료 등급에서는 콜드 스타트가 가능하다는 점에 유의하세요.
여기 내 dockerfile이 있습니다.
# syntax=docker/dockerfile:1 ARG NODE_VERSION=20.13.1 ARG PNPM_VERSION=9.4.0 FROM node:${NODE_VERSION}-bookworm AS base ## set shell to bash SHELL [ "/usr/bin/bash", "-c" ] WORKDIR /usr/src/app ## install pnpm. RUN --mount=type=cache,target=/root/.npm \ npm install -g pnpm@${PNPM_VERSION} # ------------ FROM base AS deps # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.local/share/pnpm/store to speed up subsequent builds. # Leverage bind mounts to package.json and pnpm-lock.yaml to avoid having to copy them # into this layer. RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=cache,target=/root/.local/share/pnpm/store \ pnpm install --prod --frozen-lockfile # ----------- FROM deps AS build ## downloading dev dependencies. RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=cache,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile COPY . . RUN pnpm run build # ------------- FROM base AS final ENV NODE_ENV=production USER node COPY package.json . # Copy the production dependencies from the deps stage and also # the built application from the build stage into the image. COPY --from=deps /usr/src/app/node_modules ./node_modules COPY --from=build /usr/src/app/dist ./dist EXPOSE 3000 ENTRYPOINT [ "pnpm" ] CMD ["run", "start"]WORKDIR /usr/src/app ## pnpm을 설치합니다. RUN --mount=type=cache,target=/root/.npm \ npm 설치 -g pnpm@${PNPM_VERSION} # ------------ 기본 AS deps에서 # Docker의 캐싱을 활용하려면 종속성을 별도의 단계로 다운로드하세요. # /root/.local/share/pnpm/store에 대한 캐시 마운트를 활용하여 후속 빌드 속도를 높입니다. # package.json 및 pnpm-lock.yaml에 바인드 마운트를 활용하여 복사할 필요가 없도록 합니다. # 이 레이어에 들어갑니다. RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=cache,target=/root/.local/share/pnpm/store \ pnpm install --prod --frozen-lockfile # ----------- Deps AS 빌드에서 ## 개발 종속성을 다운로드하는 중입니다. RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=cache,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile 복사 . . RUN pnpm 실행 빌드 # ------------- 기본 AS 최종에서 ENV NODE_ENV=생산 사용자 노드 패키지.json을 복사하세요. # deps 단계에서 프로덕션 종속성을 복사하고 # 빌드 단계에서 빌드된 애플리케이션을 이미지로 변환합니다. 복사 --from=deps /usr/src/app/node_modules ./node_modules 복사 --from=build /usr/src/app/dist ./dist 노출 3000 진입점
구글 클라우드 런
형식으로 바꿉니다.
이 튜토리얼은 여기까지입니다.
읽어주셔서 감사합니다 ❣️
부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.
Copyright© 2022 湘ICP备2022001581号-3