프로젝트

개발 생산성 향상을 위한 로컬-서버 자동 동기화 및 핫 리로딩 툴

Jin-Co 2025. 9. 24. 20:17
반응형

로컬에서 작업한 코드를 원격 서버에 배포할 때, 반복적인 수작업으로 시간을 소모하고 계신가요? 코드를 수정할 때마다 git push, 서버 접속(ssh), git pull 또는 docker-compose up 명령어를 수동으로 입력하는 과정은 개발 흐름을 방해할 수 있습니다.

 

만약 코드를 저장하는 즉시 원격 서버에 파일이 자동으로 동기화되고, 애플리케이션이 자동으로 재시작된다면 개발 효율성을 크게 높일 수 있을 것입니다. Node.js와 몇 가지 명령어를 활용하여 이 과정을 자동화할 수 있는 툴을 소개합니다.

 

이전글을 통해 로컬과 서버 파일연동을 통한 핫리로딩을 소개해 드렸는데요, 오늘은 nodemon처럼 사용가능한 더욱 편리한 방법을 알려 드리겠습니다.

이 툴이 해결하는 문제

이 툴은 다음과 같은 비효율적인 수작업 과정을 줄이는 것을 목표로 합니다.

  • ssh를 통한 서버 접속 시간
  • 변경된 파일을 수동으로 업로드하는 시간
  • docker-compose 명령어를 다시 실행하는 시간

이 모든 과정을 단 한 번의 node 명령어 실행으로 자동화할 수 있습니다.

솔루션: Node.js 기반 자동화 스크립트

이 툴은 Node.jschokidar 라이브러리를 사용하여 로컬 프로젝트 폴더의 변경사항을 실시간으로 감지합니다. 변경이 감지되면, tarssh를 이용해 파일을 원격 서버로 전송하고, 서버에서 docker-compose 명령어를 실행하여 애플리케이션을 재시작합니다.

이 방식은 rsync 사용 시 발생할 수 있는 환경 변수(PATH) 관련 오류를 피할 수 있는 안정적인 방법 중 하나입니다.

툴 사용을 위한 사전 준비

아래의 도구들이 로컬 머신과 원격 서버에 설치되어 있어야 합니다.

  • Node.js: 로컬에서 스크립트를 실행하기 위해 필요합니다.
  • Git Bash (Windows): tar, ssh, ls 등 리눅스 명령어를 윈도우에서 사용할 수 있게 합니다.
  • SSH 키: 비밀번호 없이 서버에 안전하게 접속하기 위해 필요합니다.
  • DockerDocker Compose: 서버에서 애플리케이션을 컨테이너로 실행하기 위해 필요합니다.
  • tar: 파일을 압축하고 스트리밍하는 데 사용합니다. (대부분의 Linux 시스템에 기본 설치되어 있습니다.)

자동화 스크립트 코드 (sync_and_reload.js)

아래 코드를 sync_and_reload.js라는 이름으로 프로젝트의 루트 디렉터리에 저장하세요. 코드에 대한 자세한 설명은 주석을 참고할 수 있습니다.

/**
 * @fileoverview 로컬 파일과 원격 서버를 동기화하고 변경사항 발생 시 서버를 핫 리로드하는 Node.js 스크립트입니다.
 * 이 툴은 개발 워크플로우를 원활하게 만들기 위해 로컬 코드 변경사항을 원격 서버로 자동 푸시합니다.
 * rsync 대신 tar와 SSH를 사용해 "command not found" 오류를 방지하는 강력한 대안입니다.
 *
 * 요구사항:
 * - 로컬 머신에 Node.js 및 npm 설치
 * - 로컬 및 서버에 tar 설치
 * - SSH 키(.pem 파일)를 이용한 서버 인증 설정
 *
 * 설정:
 * - 'config' 객체를 여러분의 서버 및 프로젝트 정보에 맞게 수정하세요.
 * - 이 스크립트를 'node sync_and_reload.js' 명령어로 실행하세요.
 * - 파일 변경을 지속적으로 감지하고 서버와 동기화합니다.
 *
 * 작성자: KBJinco
 * 버전: 1.5.0
 */

const { watch } = require('chokidar');
const { exec } = require('child_process');
const path = require('path');

// --- 환경 설정 ---
// 여러분의 환경에 맞게 아래 값들을 수정하세요.
const config = {
  sshUser: '<User>', // SSH 사용자명
  sshHost: '<IP주소>', // 서버 IP 또는 호스트명
  localPath: '.', // 감시할 로컬 디렉터리 ('.'는 현재 디렉터리)
  remotePath: '~/<폴더명>', // 동기화할 원격(서버) 디렉터리
  syncExclusions: [
    'node_modules',
    '.git',
    'dist',
    '.DS_Store',
    '*.log',
    '*.swp',
    '*.tmp',
  ], // 동기화에서 제외할 파일/폴더
  reloadCommand: 'cd ~/stock_bot && docker-compose up -d --build --force-recreate', // 서버에서 실행할 핫 리로드 명령어
  // Git Bash 셸 경로. 윈도우 사용자의 경우 필수입니다.
  shellPath: 'C:\\Program Files\\Git\\bin\\bash.exe',
  // SSH 키 파일(.pem)의 절대 경로.
  sshKeyPath: '<키_경로>, // 키 경로 - 키가 위치한 폴더에서 커맨드라인에 pwd실행
};

// --- 헬퍼 함수 ---

/**
 * 셸 명령어를 실행하고 결과를 처리합니다.
 * @param {string} command 실행할 명령어.
 * @returns {Promise<string>} 명령어 실행 결과를 담은 프로미스.
 */
const executeCommand = (command) => {
  return new Promise((resolve, reject) => {
    const isWindows = process.platform === 'win32';
    const shell = isWindows ? config.shellPath : undefined;

    exec(command, { shell }, (error, stdout, stderr) => {
      if (error) {
        console.error(`명령어 실행 오류: ${command}`);
        console.error(`오류: ${error.message}`);
        console.error(`Stderr: ${stderr}`);
        return reject(error);
      }
      if (stderr) {
        console.warn(`명령어 경고: ${command}`);
        console.warn(`Stderr: ${stderr}`);
      }
      resolve(stdout);
    });
  });
};

/**
 * tar와 ssh를 사용하여 로컬 파일을 원격 서버에 동기화합니다.
 * @param {string} changedFile 동기화를 트리거한 파일 경로.
 */
const syncFiles = async (changedFile) => {
  const localDir = path.resolve(config.localPath).replace(/\\/g, '/');
  console.log(`\n변경 감지: ${changedFile}`);
  console.log('tar over SSH를 사용하여 파일 동기화 중...');

  const tarExclude = config.syncExclusions.map(e => `--exclude='${e}'`).join(' ');
  const tarCommand = `tar -czf - ${tarExclude} .`;

  const remoteCommand = `mkdir -p ${config.remotePath} && tar -xzf - -C ${config.remotePath}`;

  const sshOptions = `-i "${config.sshKeyPath.replace(/\\/g, '/')}"`;
  const fullCommand = `${tarCommand} | ssh ${sshOptions} ${config.sshUser}@${config.sshHost} "${remoteCommand}"`;

  try {
    await executeCommand(fullCommand);
    console.log('파일 동기화 성공.');
    triggerReload();
  } catch (err) {
    console.error('파일 동기화 실패.');
  }
};

/**
 * SSH를 통해 원격 서버에서 핫 리로드 명령어를 트리거합니다.
 */
const triggerReload = async () => {
  console.log('서버에서 핫 리로딩 트리거 중...');
  const sshOptions = `-i "${config.sshKeyPath.replace(/\\/g, '/')}"`;
  const sshCommand = `ssh ${sshOptions} ${config.sshUser}@${config.sshHost} "${config.reloadCommand}"`;

  try {
    const output = await executeCommand(sshCommand);
    console.log('서버 핫 리로딩 성공.');
    console.log('서버 출력:\n', output);
  } catch (err) {
    console.error('서버 핫 리로딩 실패.');
  }
};

/**
 * 파일 감시 및 동기화 프로세스를 시작하는 메인 함수.
 */
const startWatcher = () => {
  console.log('파일 감시 시작...');
  console.log(`감시 중인 디렉터리: ${path.resolve(config.localPath)}`);
  console.log(`동기화 대상 서버: ${config.sshUser}@${config.sshHost}:${config.remotePath}`);
  console.log(`리로드 명령어: ${config.reloadCommand}`);

  const watcher = watch(config.localPath, {
    ignored: config.syncExclusions.map(e => `**/${e}/**`),
    ignoreInitial: true,
    persistent: true,
  });

  watcher.on('add', (filePath) => syncFiles(filePath));
  watcher.on('change', (filePath) => syncFiles(filePath));
  watcher.on('unlink', (filePath) => syncFiles(filePath));

  console.log('감시 준비 완료. 파일 변경을 기다리는 중...');
};

startWatcher();

서버설정

핫리로딩이 작동하기 위해서는 서버에 rsync가 설치되어야 합니다. 서버 접속후

 

로컬설정

  1. Node.js 라이브러리 설치: sync_and_reload.js를 저장한 프로젝트 폴더에서 터미널을 열고 npm install chokidar 명령어를 실행하여 필요한 라이브러리를 설치합니다.
npm install chokidar
  1. config 객체 수정: sync_and_reload.js 파일 상단에 있는 config 객체를 여러분의 서버 정보와 프로젝트 경로에 맞게 수정하세요. sshKeyPath에는 .pem 파일의 절대 경로를 정확히 입력해야 합니다.
  2. 스크립트 실행: 터미널에서 (스트립트가 위치한 폴더에서) node sync_and_reload.js 명령어를 실행하면, 스크립트가 로컬 파일 변경을 실시간으로 감지하기 시작합니다.
node sync_and_reload.js

이제 로컬에서 코드를 저장할 때마다, 서버로 파일이 전송되고 Docker 컨테이너가 자동으로 재시작되는 코드가 완성되었습니다. 이 툴을 통해 반복적인 수작업에서 벗어나 코드 작성에 집중할 수 있겠네요.

728x90
반응형