Node.js를 사용한 분산 비디오 트랜스코딩 시스템 구축

발행: (2025년 12월 17일 오후 09:52 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Node.js를 사용한 분산 비디오 트랜스코딩 시스템 구축 커버 이미지

왜 브로커인가?

브로커는 두 가지 이유로 분산 시스템의 “hello world”입니다:

  1. 쉽게 시작하고 실행할 수 있음 – 이들은 hive / master‑node 패턴을 강제하여 자연스럽게 확장됩니다.

    node                
    node    hive / broker   client‑facing server    client
    node                                                client

순수 JavaScript 브로커 for Node.js

// broker.js
import Bunny from "bunnimq";
import path from "path";
import { fileURLToPath } from "url";

Bunny({
  port: 3000,
  DEBUG: true,
  cwd: path.dirname(fileURLToPath(import.meta.url)), // path to the .auth file
  queue: {
    Durable: true,
    MessageExpiry: 60 // 1 hour
  }
});

로우레벨 Node.js 최적화

  • Object → 바이너리 컴파일러
  • SharedArrayBuffers 및 워커 스레드
const buffer = new SharedArrayBuffer();
const worker = new Worker(); // **Prerequisite:** FFmpeg는 설치되어 있어야 하며 `PATH`에 있어야 합니다. 테스트해 보세요:
ffmpeg -i img.jpg img.png

Source:

분산 비디오 트랜스코딩 예제

프로젝트 초기화

npm init -y && npm i bunnimq bunnimq-driver

폴더 구조

ffmpegserver/
  server.js    # ← 하이브(브로커)
  producer.js  # 클라이언트용 서버
  consumer.js  # 노드 서버 / 워커
  .auth        # 프로듀서와 컨슈머 인증을 위한 자격 증명(.env와 유사)

.auth

다음 형식으로 비밀 자격 증명을 입력하세요: username:password:privileges (특권은 레포지토리에서 확인).

sk:mypassword:4
jane:doeeee:1
john:doees:3

Server (Hive) – server.js

간단한 비‑TLS 설정 (TLS는 지원됩니다 – GitHub 저장소를 참고하세요):

import Bunny from "bunnimq";
import path from "path";
import { fileURLToPath } from "url";

Bunny({
  port: 3000,
  DEBUG: true,
  cwd: path.dirname(fileURLToPath(import.meta.url)), // for .auth file
  queue: {
    Durable: true,
    QueueExpiry: 0,
    MessageExpiry: 3600
  }
});

Producer – producer.js

서버 브라우저와 기타 클라이언트가 통신하는 대상입니다. 요청을 받아 작업을 hive에 푸시합니다.

import BunnyMQ from "bunnimq-driver";
import fs from "node:fs/promises";

const bunny = new BunnyMQ({
  port: 3000,
  host: "localhost",
  username: "sk",
  password: "mypassword",
});

큐 선언 (존재하지 않을 경우)

bunny.queueDeclare(
  {
    name: "transcode_queue",
    config: {
      QueueExpiry: 60,
      MessageExpiry: 20,
      AckExpiry: 10,
      Durable: true,
      noAck: false,
    },
  },
  (res) => {
    console.log("Queue creation:", res);
  }
);

비디오 트랜스코딩 작업 퍼블리시

데모에서는 로컬 폴더에서 비디오를 읽어옵니다 (경로를 본인 환경에 맞게 교체하세요):

async function processVideos() {
  const videos = await fs.readdir(
    "C:/Users/[path to a folder with videos]/Videos/Capcut/test"
  ); // 보통은 스토리지 버킷 링크

  for (const video of videos) {
    const job = {
      id: Date.now() + Math.random().toString(36).substring(2),
      input: `C:/Users/[path to a folder with videos]/Videos/Capcut/test/${video}`,
      outputFormat: "webm",
    };

    // 큐에 넣기
    bunny.publish("transcode_queue", JSON.stringify(job), (res) => {
      console.log(`Job ${job.id} published:`, res ? "ok" : "400");
    });
  }
}

processVideos();

소비자 – consumer.js

이들은 작업을 가져오고, 비디오를 트랜스코딩하며, 완료를 확인하는 워커 노드입니다.

transcode_queue를 소비

import BunnyMQ from "bunnimq-driver";
import { spawn } from "child_process";
import path from "path";

const bunny = new BunnyMQ({
  port: 3000,
  host: "localhost",
  username: "john",
  password: "doees",
});
bunny.consume("transcode_queue", async (msg) => {
  console.log("Received message:", msg);

  try {
    const { input, outputFormat } = JSON.parse(msg);

    // Normalise paths
    const absInput = path.resolve(input);
    const output = absInput.replace(/\.[^.]+$/, `.${outputFormat}`);

    console.log(
      `Spawning: ffmpeg -i "${absInput}" -f ${outputFormat} "${output}" -y`
    );

    await new Promise((resolve, reject) => {
      const ffmpeg = spawn(
        "ffmpeg",
        ["-i", absInput, "-f", outputFormat, output, "-y"],
        { shell: true } // helps Windows find ffmpeg.exe
      );

      ffmpeg.on("error", reject);

      // FFmpeg logs to stderr
      ffmpeg.stderr.on("data", (chunk) => {
        process.stderr.write(chunk);
      });

      ffmpeg.on("close", (code) => {
        if (code === 0) {
          console.log(`Transcoding complete: ${output}`);
          // Acknowledge the message
          bunny.Ack((ok) => console.log("Ack sent:", ok));
          resolve();
        } else {
          reject(new Error(`FFmpeg exited with code ${code}`));
        }
      });
    });
  } catch (err) {
    console.error("Failed to process job:", err);
    // Optionally reject / requeue the message here
  }
});

시스템 실행

  1. 브로커 시작

    node server.js
  2. 하나 이상의 컨슈머 시작 (별도의 터미널에서)

    node consumer.js
  3. 작업 게시

    node producer.js

각 컨슈머가 큐에서 작업을 가져와 FFmpeg를 호출하고 완료를 확인하는 것을 볼 수 있습니다. 스케일링은 브로커에 접근할 수 있는 동일한 머신이나 다른 머신에서 추가 consumer.js 프로세스를 실행하는 것만큼 간단합니다.

행복한 트랜스코딩!

나에 대한 더 많은 글

여기서 찾아보세요

Back to Blog

관련 글

더 보기 »

Node.js에서 Bulkhead Pattern 구현하기

시스템 복원력 소개: 분산 시스템(distributed system)이나 마이크로서비스 아키텍처(microservices architecture)에서는 단일 실패 컴포넌트가 파급 효과(ripple effect)를 일으켜 다운타임(downtime)을 초래할 수 있습니다.