6편에서 fs 로 파일을 다뤘다. 그런데 파일을 다루다 보면 곧 — "경로" 문제에 부딪친다. 맥·리눅스는 /home/user/file.txt 인데 윈도우는 C:\Users\user\file.txt. 슬래시 방향이 반대다. 직접 문자열로 합치면 한쪽이 무조건 깨진다.
이걸 해결하는 게 path 모듈. 그리고 비슷한 결로 시스템 정보(메모리·CPU·홈 디렉토리)를 얻는 게 os 모듈. 둘 다 작지만 거의 모든 Node 프로그램이 쓴다.
1. path — 크로스 플랫폼 경로의 안전망
가장 자주 부딪치는 실수.
// ❌ 직접 문자열로 합치기 (윈도우에서 깨짐)
const file = './data' + '/' + 'users.json';
// ✅ path.join 사용 (어디서나 동작)
import path from 'node:path';
const file = path.join('.', 'data', 'users.json');
path.join 은 OS 에 맞는 구분자(/ 또는 \)를 자동으로 골라준다. 또 연속 슬래시·.. 같은 것도 정규화. 직접 문자열을 합치는 순간 크로스 플랫폼 보장이 깨진다고 보면 된다.
2. path 자주 쓰는 메서드 6가지
import path from 'node:path';
// ① 합치기 — 가장 많이 씀
path.join('a', 'b', 'c'); // 'a/b/c'
path.join('/home', 'user', 'docs'); // '/home/user/docs'
path.join('a', '..', 'b'); // 'b' (정규화됨)
// ② 절대 경로로 변환
path.resolve('a/b'); // '/현재/작업/디렉토리/a/b'
path.resolve('/x', 'y'); // '/x/y'
// ③ 파일 이름 / 확장자 / 디렉토리 분리
path.basename('/home/user/file.txt'); // 'file.txt'
path.basename('/home/user/file.txt', '.txt'); // 'file'
path.dirname('/home/user/file.txt'); // '/home/user'
path.extname('/home/user/file.txt'); // '.txt'
// ④ 객체로 한 번에 분해
path.parse('/home/user/file.txt');
// { root: '/', dir: '/home/user', base: 'file.txt', name: 'file', ext: '.txt' }
// ⑤ 두 경로의 상대 경로
path.relative('/data/orders', '/data/orders/2026/01.json');
// '2026/01.json'
// ⑥ 구분자 자체 (윈도우 \, 그 외 /)
path.sep; // '/' or '\\'
join vs resolve 차이 — join 은 단순히 합쳐 정규화하고, resolve 는 절대 경로로 만든다. 파일 시스템 작업엔 거의 resolve 가 안전 (현재 작업 디렉토리에 안 휘둘림). 단 URL 만들 땐 join 이 편함.
3. __dirname 함정과 ESM 대안
CommonJS 시절엔 __dirname 이 자동 변수로 들어왔다. 현재 파일이 있는 디렉토리. ESM("type": "module")에선 사라졌다.
// ❌ ESM 에선 ReferenceError
const config = path.join(__dirname, 'config.json');
// ✅ 표준 ESM 패턴
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const config = path.join(__dirname, 'config.json');
이 4줄이 사실상 모든 ESM Node 파일에 등장하는 보일러플레이트. 보고 외워두면 좋다. Node 21 부터는 import.meta.dirname 한 줄로 단축됐다.
// Node 21+ (현행 LTS 22 는 사용 가능)
const __dirname = import.meta.dirname;
4. os — 시스템 정보 얻기
os 모듈은 CPU·메모리·홈 디렉토리·플랫폼·hostname 같은 시스템 정보 창구.
import os from 'node:os';
os.platform(); // 'darwin' | 'linux' | 'win32'
os.arch(); // 'x64' | 'arm64'
os.cpus().length; // CPU 코어 수 (예: 8)
os.totalmem(); // 전체 메모리 (바이트)
os.freemem(); // 가용 메모리 (바이트)
os.homedir(); // '/home/user' or 'C:\\Users\\user'
os.tmpdir(); // '/tmp' or 'C:\\Users\\...\\Temp'
os.hostname(); // 'mac-mini-of-jspark'
os.userInfo(); // { username, uid, gid, shell, homedir }
os.EOL; // '\n' or '\r\n' — 줄바꿈 문자
가장 유용한 두 가지 — os.homedir() (사용자 홈 폴더, 설정 파일 위치 계산용) 와 os.cpus().length (워커 풀 크기 정할 때). os.EOL 도 의외로 자주 — 윈도우 줄바꿈 호환 처리.
5. 실전 예 — 크로스 플랫폼 설정 파일
두 모듈을 같이 쓰는 표준 패턴.
// ~/.myapp/config.json 읽기·생성
import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
const APP_DIR = path.join(os.homedir(), '.myapp');
const CONFIG_FILE = path.join(APP_DIR, 'config.json');
async function loadConfig() {
try {
return JSON.parse(await fs.readFile(CONFIG_FILE, 'utf-8'));
} catch (e) {
if (e.code === 'ENOENT') {
await fs.mkdir(APP_DIR, { recursive: true });
const defaults = { theme: 'dark', lastSync: null };
await fs.writeFile(CONFIG_FILE, JSON.stringify(defaults, null, 2));
return defaults;
}
throw e;
}
}
const config = await loadConfig();
console.log(`Loaded config from: ${CONFIG_FILE}`);
맥·리눅스에선 ~/.myapp/config.json, 윈도우에선 C:\Users\xx\.myapp\config.json — 코드 한 줄도 안 바꾸고 둘 다 동작. os.homedir() + path.join() 의 조합이 핵심.
윈도우 함정 — 경로의 백슬래시 — JSON·정규식·문자열 리터럴에 윈도우 경로를 넣을 땐 \\ 두 번. 이런 거 신경 쓰기 싫으면 무조건 path.join·path.resolve 만 쓰면 된다. 직접 백슬래시를 타이핑하는 순간 함정.
요약 — 7편 좌표
여기까지 정리. 경로는 절대 직접 문자열로 합치지 말고 path.join·path.resolve 사용. ESM 에선 __dirname 대신 import.meta.dirname (Node 21+) 또는 fileURLToPath 패턴. os.homedir()·os.cpus()·os.tmpdir() 같은 시스템 정보. 설정 파일은 path.join(os.homedir(), '.myapp') 가 크로스 플랫폼 표준. 다음 편에선 Node 의 심장 — 이벤트 루프 의 동작을 본다.
다음 편 예고 — 이벤트 루프 완전 정리
Call Stack · Queue · libuv 가 어떻게 함께 도나. 8편.