Node.js · 기본
Node.js로 만드는 작은 API
Node.js 실무: JSON 설정 파일을 읽고 잘못된 값은 초기에 거절하기
Node.js의 비동기 파일 읽기와 JSON 파싱을 분리하고 타입·범위 검증으로 잘못된 설정이 런타임까지 흘러가지 않게 한다.
개발자 · 원고 갱신
문제에서 시작하기
수집기의 동시 요청 수를 설정 파일로 바꾸었는데 운영에서 문자열 "8"이 들어왔다. JSON 파싱은 성공했지만 나중에 계산에서 예상하지 못한 결과가 생긴다. 파싱 성공은 JSON 문법이 맞다는 뜻일 뿐 프로그램의 계약이 맞다는 뜻은 아니다. 파일 읽기, 문법 해석, 값 검증을 나눠 실패 지점을 명확히 만든다.
그림. 오류가 발생하는 단계를 분리했다. 필수 설정이 잘못되면 빈 객체로 성공을 가장하지 않는다. Devin.KR이 직접 제작한 학습용 SVG 개념도입니다. 이미지를 선택하면 크게 볼 수 있습니다.
학습 목표와 선수지식
JavaScript 객체와 async/await를 알고 있다고 가정한다. 파일 실패와 검증 실패를 구분하고, 설정을 검증한 이후에만 호출자에게 전달하는 작은 경계를 만든다.
실행 환경
Node.js 22 이상, 외부 패키지 없음. node config-check.mjs. 임시 디렉터리에 자체 테스트 파일을 만들고 끝나면 그 디렉터리만 제거한다.
직접 실행하기
config-check.mjs에 저장한다.
import { readFile, writeFile, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import assert from 'node:assert/strict';
async function loadConfig(path) {
const raw = JSON.parse(await readFile(path, 'utf8'));
if (raw === null || Array.isArray(raw) || typeof raw !== 'object') {
throw new TypeError('config must be an object');
}
if (!Number.isInteger(raw.concurrency) ||
raw.concurrency < 1 || raw.concurrency > 8) {
throw new RangeError('concurrency must be an integer from 1 to 8');
}
return Object.freeze({ concurrency: raw.concurrency });
}
const dir = await mkdtemp(join(tmpdir(), 'devin-config-'));
try {
const file = join(dir, 'config.json');
await writeFile(file, '{"concurrency":4}', 'utf8');
const config = await loadConfig(file);
assert.equal(config.concurrency, 4);
console.log('concurrency=' + config.concurrency);
await writeFile(file, '{"concurrency":"4"}', 'utf8');
await assert.rejects(() => loadConfig(file), RangeError);
await writeFile(file, '{"concurrency":0}', 'utf8');
await assert.rejects(() => loadConfig(file), RangeError);
await writeFile(file, '{broken', 'utf8');
await assert.rejects(() => loadConfig(file), SyntaxError);
await assert.rejects(() => loadConfig(join(dir, 'missing.json')),
{ code: 'ENOENT' });
console.log('invalid inputs rejected');
} finally {
await rm(dir, { recursive: true, force: true });
}
예상 결과
concurrency=4
invalid inputs rejected
코드를 읽는 순서
.mjs 확장자는 이 파일을 ES 모듈로 읽게 한다. 모듈 최상위에서 await를 사용할 수 있으며 require와 뒤섞지 않는다. readFile은 UTF-8 문자열을 반환하도록 인코딩을 지정했다. 생략하면 Buffer가 되어 입력 의미가 흐려진다.
loadConfig의 첫 단계는 파일을 읽고 JSON 문법을 해석하는 것이다. 다음 단계에서는 최상위 값이 객체인지 검사하고 정수와 범위를 확인한다. 배열과 null도 유효한 JSON이지만 이 프로그램의 설정 객체는 아니다.
숫자 문자열을 Number로 무조건 바꾸지 않는다. 운영 설정에 잘못된 타입이 들어왔을 때 조용히 보정하면 생성 측의 오류가 오래 살아남는다. 타입 강제 변환이 실제 계약이라면 허용 형식과 공백·빈 문자열 처리부터 별도로 정의한다.
반환 객체는 필요한 필드만 담는다. 원본 설정의 임의 속성을 내부 코드로 퍼뜨리지 않으며 Object.freeze로 이번 평평한 객체의 변경을 막는다. 중첩 설정이 생기면 깊은 불변성은 별도 문제다. 시작 단계에서 오류를 상위로 전달해 서비스를 정상 상태처럼 띄우지 않도록 한다.
흔한 오류와 반례
- catch에서 빈 객체를 반환하면 실패가 성공처럼 바뀐다. 필수 설정은 잘못된 값으로 계속 실행하기보다 오류를 전달한다.
- readFile은 파일 전체를 메모리에 읽는다. 이 예제는 로컬의 작은 신뢰된 설정용이며 대용량 업로드 처리 예제가 아니다.
- 에러 로그에 설정 전체를 출력하면 토큰이나 비밀번호가 노출될 수 있다. 필드 이름과 오류 유형 정도로 진단한다.
연습문제
concurrency를 1, 8, 9, 1.5, null로 바꿔 경계값을 확인한다. 선택 필드 mode의 기본값을 safe로 두되, 문자열 safe 또는 fast만 허용하려면 어떻게 작성할까?
정답과 해설
1과 8만 정상이며 9·1.5·null은 거절한다. mode는 const mode = raw.mode === undefined ? "safe" : raw.mode;로 기본값을 주고 허용 목록에 있는지 검사한다. raw.mode || "safe"는 빈 문자열까지 기본값으로 숨기므로 입력 오류를 구분하지 못한다.
참고자료와 작성 정보
Devin.KR AI 작성 · 2026-09-12. 독립적으로 작성한 설명과 가상 데이터 예제다. 실행 결과와 경계 조건을 검증했으며, 공개 전 편집 검토 대상이다. 특정 서적의 번역·발췌·요약본이 아니다.
READER FEEDBACK
질문·오탈자·의견
내용에 관한 질문이나 오탈자, 더 나은 설명을 위한 의견을 남겨 주세요. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.