개발 생산성 향상을 위한 로컬-서버 자동 동기화 및 핫 리로딩 툴
로컬에서 작업한 코드를 원격 서버에 배포할 때, 반복적인 수작업으로 시간을 소모하고 계신가요? 코드를 수정할 때마다 git push
, 서버 접속(ssh
), git pull
또는 docker-compose up
명령어를 수동으로 입력하는 과정은 개발 흐름을 방해할 수 있습니다.
만약 코드를 저장하는 즉시 원격 서버에 파일이 자동으로 동기화되고, 애플리케이션이 자동으로 재시작된다면 개발 효율성을 크게 높일 수 있을 것입니다. Node.js
와 몇 가지 명령어를 활용하여 이 과정을 자동화할 수 있는 툴을 소개합니다.
이전글을 통해 로컬과 서버 파일연동을 통한 핫리로딩을 소개해 드렸는데요, 오늘은 nodemon처럼 사용가능한 더욱 편리한 방법을 알려 드리겠습니다.
이 툴이 해결하는 문제
이 툴은 다음과 같은 비효율적인 수작업 과정을 줄이는 것을 목표로 합니다.
ssh
를 통한 서버 접속 시간- 변경된 파일을 수동으로 업로드하는 시간
docker-compose
명령어를 다시 실행하는 시간
이 모든 과정을 단 한 번의 node
명령어 실행으로 자동화할 수 있습니다.
솔루션: Node.js
기반 자동화 스크립트
이 툴은 Node.js
의 chokidar
라이브러리를 사용하여 로컬 프로젝트 폴더의 변경사항을 실시간으로 감지합니다. 변경이 감지되면, tar
와 ssh
를 이용해 파일을 원격 서버로 전송하고, 서버에서 docker-compose
명령어를 실행하여 애플리케이션을 재시작합니다.
이 방식은 rsync
사용 시 발생할 수 있는 환경 변수(PATH
) 관련 오류를 피할 수 있는 안정적인 방법 중 하나입니다.
툴 사용을 위한 사전 준비
아래의 도구들이 로컬 머신과 원격 서버에 설치되어 있어야 합니다.
Node.js
: 로컬에서 스크립트를 실행하기 위해 필요합니다.Git Bash
(Windows):tar
,ssh
,ls
등 리눅스 명령어를 윈도우에서 사용할 수 있게 합니다.SSH
키: 비밀번호 없이 서버에 안전하게 접속하기 위해 필요합니다.Docker
및Docker 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가 설치되어야 합니다. 서버 접속후
로컬설정
Node.js
라이브러리 설치:sync_and_reload.js
를 저장한 프로젝트 폴더에서 터미널을 열고npm install chokidar
명령어를 실행하여 필요한 라이브러리를 설치합니다.
npm install chokidar
config
객체 수정:sync_and_reload.js
파일 상단에 있는config
객체를 여러분의 서버 정보와 프로젝트 경로에 맞게 수정하세요.sshKeyPath
에는.pem
파일의 절대 경로를 정확히 입력해야 합니다.- 스크립트 실행: 터미널에서 (스트립트가 위치한 폴더에서)
node sync_and_reload.js
명령어를 실행하면, 스크립트가 로컬 파일 변경을 실시간으로 감지하기 시작합니다.
node sync_and_reload.js
이제 로컬에서 코드를 저장할 때마다, 서버로 파일이 전송되고 Docker
컨테이너가 자동으로 재시작되는 코드가 완성되었습니다. 이 툴을 통해 반복적인 수작업에서 벗어나 코드 작성에 집중할 수 있겠네요.