PHpullh

TYPESCRIPT · 심층 가이드

TypeScript 파일/IO 완전 정리

TypeScript 자체에는 파일이라는 개념이 없어서, 읽고 쓰는 코드는 전부 Node나 Deno 같은 런타임 API와 그 타입 정의에 기대게 됩니다.

주제 15개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

언어 스펙에 표준 입출력이 없다는 점이 이 영역의 성격을 그대로 결정합니다. 브라우저에서 도는 같은 문법의 코드에는 fs가 아예 없고, Node에서 쓰려면 @types/node를 설치해야 컴파일러가 process와 node:fs를 알아봅니다. 모듈 방식에 따라 require와 import가 갈리고 __dirname을 쓸 수 있는지도 달라지므로, 코드보다 설정에서 먼저 막히는 일이 흔한 구간입니다.

텍스트 파일 읽기와 텍스트 파일 쓰기로 경로와 인코딩을 다루는 감을 잡은 다음, 구조가 있는 데이터로 넘어가 JSON 저장과 CSV 파싱을 나란히 비교해 보면 직렬화 형식마다 무엇을 보존하고 무엇을 잃는지가 눈에 들어옵니다. 환경 변수 읽기와 명령행 인자는 파일 바깥에서 들어오는 입력을 다루는 주제이고, 이 셋을 한데 묶어 어느 쪽 값을 우선할지 규칙을 정하는 마지막 단계가 설정 로드입니다. 순서를 지키면 뒤로 갈수록 새로 배울 것이 줄어듭니다.

가장 위험한 구멍은 JSON.parse의 반환 타입이 any라는 사실입니다. 여기에 인터페이스를 단언으로 붙이면 컴파일러는 만족하지만 실제 파일 내용은 아무도 검사하지 않았고, 필드 하나가 빠진 채 한참 뒤 엉뚱한 곳에서 undefined로 터집니다. 파싱 직후 좁히는 가드를 두는 편이 낫습니다. 인코딩을 생략한 readFile이 문자열이 아니라 Buffer를 돌려준다는 점도 함께 기억할 만합니다.

01텍스트 파일 읽기

node:fs의 readFileSync로 package.json을 UTF-8 문자열로 읽은 뒤 앞 20자만 잘라 출력합니다. 프로그램 시작 시점에 작은 설정 파일을 한 번 읽는 정도라면 이 형태로 충분합니다. 세 번째 인자인 인코딩을 빼면 반환 타입이 string이 아니라 Buffer가 되어 slice의 의미까지 달라집니다.

TypeScript code

// 텍스트 파일 읽기
import { readFileSync } from "node:fs";
console.log(readFileSync("package.json", "utf8").slice(0, 20));
알아두면 좋은 점

readFileSync는 인코딩 문자열 대신 { encoding: 'utf8', flag: 'r' } 객체도 받으므로, 여는 방식을 함께 지정할 때 유용합니다.

자주 하는 실수

파일이 없으면 ENOENT 예외가 그대로 올라와 프로세스가 끝나므로, 선택적으로 존재하는 설정 파일이라면 감싸서 잡아야 합니다.

02텍스트 파일 쓰기

writeFileSync가 sample-2.txt에 "hello\n" 한 줄을 UTF-8로 기록합니다. 빌드 산출물이나 테스트 픽스처처럼 작고 한 번에 끝나는 쓰기에 적합한 방식입니다. 기본 플래그가 w라서 같은 이름의 파일이 있으면 내용을 이어붙이는 대신 통째로 잘라내고 덮어씁니다.

TypeScript code

// 텍스트 파일 쓰기
import { writeFileSync } from "node:fs";
writeFileSync("sample-2.txt", "hello\n", "utf8");
알아두면 좋은 점

덧붙여 쓰려면 { flag: 'a' }를 주면 되고, 기존 파일을 덮어쓰지 않게 하려면 'wx'로 열어 충돌 시 예외를 받을 수 있습니다.

자주 하는 실수

쓰는 도중 프로세스가 죽으면 잘린 파일이 남으므로, 중요한 산출물은 임시 이름으로 쓴 뒤 renameSync로 바꾸는 편이 안전합니다.

03텍스트 파일 추가 쓰기

path.join이 logs, 2026, app-3.log 세 조각을 플랫폼 구분자로 이어 하나의 상대 경로 문자열을 만듭니다. 날짜별 로그처럼 경로 일부가 실행 중에 정해질 때 문자열 덧셈 대신 씁니다. join은 경로 문자열만 계산할 뿐 디렉터리를 만들지도, 존재 여부를 확인하지도 않습니다.

TypeScript code

// 텍스트 파일 추가 쓰기
import path from "node:path";
console.log(path.join("logs", "2026", "app-3.log"));
알아두면 좋은 점

path.join은 조각 사이의 중복 구분자를 정리해 주므로, 각 값의 앞뒤에 슬래시가 붙었는지 신경 쓰지 않고 그대로 넘길 수 있습니다.

자주 하는 실수

사용자 입력을 조각으로 넘기면 ..가 상위 디렉터리로 빠져나갈 수 있으므로, 합친 뒤 기준 경로로 시작하는지 확인해야 합니다.

04JSON 저장

process.env.NODE_ENV와 process.argv.slice(2)를 객체로 묶어 한 번에 출력하며, 환경 변수가 없으면 development로 대체합니다. 실행 환경과 전달된 인자를 동시에 확인해야 하는 CLI 진입점에서 쓰는 형태입니다. ??는 undefined와 null만 걸러내므로 빈 문자열로 설정된 변수는 그대로 통과합니다.

TypeScript code

// JSON 저장
console.log({ env: process.env.NODE_ENV ?? "development", args: process.argv.slice(2) });
알아두면 좋은 점

process.env의 타입은 값이 string | undefined인 사전이라, 어떤 변수든 쓰기 전에 기본값이나 존재 확인이 반드시 필요합니다.

자주 하는 실수

환경 변수를 빈 문자열로 설정하면 ??를 통과해 그대로 쓰이므로, 환경 구분 분기가 어느 쪽에도 걸리지 않게 됩니다.

05CSV 파싱

여기서는 파일을 읽어 앞 20자만 확인할 뿐, 쉼표 분리나 헤더 해석 같은 파싱 단계는 전혀 하지 않습니다. CSV를 본격적으로 다루기 전에 구분자와 첫 줄 모양을 눈으로 훑는 단계에 어울립니다. slice는 UTF-16 코드 단위로 세기 때문에 이모지나 결합 문자가 경계에서 반쪽만 남을 수 있습니다.

TypeScript code

// CSV 파싱
import { readFileSync } from "node:fs";
console.log(readFileSync("package.json", "utf8").slice(0, 20));
알아두면 좋은 점

쉼표로 나누기 전에 첫 줄을 눈으로 확인하면, 구분자가 세미콜론이거나 앞에 BOM이 붙은 파일을 미리 걸러낼 수 있습니다.

자주 하는 실수

따옴표로 감싼 필드 안의 쉼표까지 split(',')이 잘라 버리므로, 값 하나가 두 칸으로 밀려 그 뒤 열이 전부 어긋납니다.

06환경 변수 읽기

동기 방식으로 sample-6.txt를 만들고 짧은 문자열 하나를 써 넣는 코드입니다. 환경 변수로 받은 값을 파일에 남기는 스크립트라면 이 호출이 마지막 단계가 됩니다. 쓰기가 끝났다고 디스크까지 반영된 것은 아니며, 확실한 보장이 필요하면 파일 디스크립터를 열고 fsyncSync를 불러야 합니다.

TypeScript code

// 환경 변수 읽기
import { writeFileSync } from "node:fs";
writeFileSync("sample-6.txt", "hello\n", "utf8");
알아두면 좋은 점

동기 쓰기는 호출이 돌아온 시점에 커널에 넘겨진 상태이며, 디스크 반영까지 보장하려면 파일 디스크립터를 열고 fsyncSync를 불러야 합니다.

자주 하는 실수

환경 변수 값을 그대로 파일에 남기면 토큰이나 비밀번호가 산출물과 함께 저장소에 올라갈 수 있으므로, 기록 전에 걸러 낼 목록을 정해야 합니다.

07명령행 인자

인자 세 개를 받아 logs/2026/app-7.log 형태의 경로 문자열을 조립해 찍습니다. 명령행 인자로 받은 이름을 디렉터리 이름과 합쳐야 할 때 이 방식이 안전합니다. 윈도우에서는 결과 구분자가 역슬래시로 나오므로, 출력 문자열을 / 기준으로 다시 쪼개는 코드를 붙이면 그쪽에서 깨집니다.

TypeScript code

// 명령행 인자
import path from "node:path";
console.log(path.join("logs", "2026", "app-7.log"));
알아두면 좋은 점

화면 출력이나 URL처럼 항상 슬래시가 필요하다면 path.posix.join을 쓰면 플랫폼과 무관하게 같은 형태의 문자열을 얻습니다.

자주 하는 실수

결과 경로를 split('/')로 다시 쪼개는 코드는 윈도우에서 역슬래시 때문에 통째로 한 조각이 되어, 조용히 잘못된 값을 만듭니다.

08경로 합치기

환경 변수와 인자 배열을 담은 객체 하나를 만들어 콘솔에 넘깁니다. 스크립트가 어떤 값을 받고 시작했는지 로그로 남겨 두면 재현이 쉬워집니다. argv의 0번은 node 실행 파일, 1번은 스크립트 경로이므로 사용자가 준 값만 보려면 slice(2)가 필요합니다.

TypeScript code

// 경로 합치기
console.log({ env: process.env.NODE_ENV ?? "development", args: process.argv.slice(2) });
알아두면 좋은 점

node:util의 parseArgs를 쓰면 옵션과 위치 인자를 직접 나누지 않아도 되고, 값 타입까지 선언으로 지정할 수 있습니다.

자주 하는 실수

slice(2)를 빼먹으면 첫 인자가 실행 파일 경로가 되어, 인자 개수 검사와 파일명 처리가 모두 한 칸씩 밀립니다.

09디렉터리 목록

고정된 파일 하나를 동기로 열어 내용 일부를 출력할 뿐, 디렉터리를 순회하는 코드는 들어 있지 않습니다. 목록 처리 로직을 붙이기 전에 대상이 실제로 읽히는지 확인하는 단계로 보면 됩니다. readFileSync는 완료될 때까지 이벤트 루프를 세우므로 요청 처리 경로에서는 node:fs/promises 쪽이 낫습니다.

TypeScript code

// 디렉터리 목록
import { readFileSync } from "node:fs";
console.log(readFileSync("package.json", "utf8").slice(0, 20));
알아두면 좋은 점

readdirSync(dir, { withFileTypes: true })는 이름과 함께 항목 종류를 돌려주어, 파일인지 확인하려고 다시 상태를 조회하지 않아도 됩니다.

자주 하는 실수

동기 읽기는 완료될 때까지 이벤트 루프를 멈추므로, 요청 처리 중에 쓰면 그 시간 동안 다른 요청이 하나도 진행되지 않습니다.

10임시 파일

sample-10.txt라는 이름을 그대로 써서 파일을 생성합니다. 임시 산출물을 남길 때도 경로가 어디에 잡히는지 알고 써야 합니다. 상대 경로는 소스 파일 위치가 아니라 process.cwd()를 기준으로 해석되므로, 실행 디렉터리가 바뀌면 파일이 엉뚱한 곳에 생깁니다.

TypeScript code

// 임시 파일
import { writeFileSync } from "node:fs";
writeFileSync("sample-10.txt", "hello\n", "utf8");
알아두면 좋은 점

os.tmpdir()과 mkdtempSync를 쓰면 충돌하지 않는 임시 디렉터리를 얻을 수 있어, 같은 스크립트를 여러 개 동시에 돌려도 안전합니다.

자주 하는 실수

상대 경로는 process.cwd() 기준이라, 같은 스크립트를 다른 디렉터리에서 실행하면 파일이 엉뚱한 곳에 생기고 이전 결과를 찾지 못합니다.

11존재 여부 확인

경로 조각을 이어 붙여 문자열을 만들 뿐 존재 여부 검사는 하지 않는 코드입니다. 확인 로직을 붙이려면 이렇게 만든 경로를 그다음 단계로 넘기게 됩니다. path.join은 중간의 ..를 정리해 주지만 결과는 여전히 상대 경로이며, 절대 경로가 필요하면 path.resolve를 써야 합니다.

TypeScript code

// 존재 여부 확인
import path from "node:path";
console.log(path.join("logs", "2026", "app-11.log"));
알아두면 좋은 점

path.resolve는 현재 작업 디렉터리를 앞에 붙여 절대 경로를 만들어 주므로, 로그에 남길 경로는 이 형태여야 나중에 재현할 수 있습니다.

자주 하는 실수

존재를 먼저 확인하고 여는 방식은 그 사이에 파일이 사라질 수 있으므로, 그냥 열고 ENOENT를 잡는 편이 확실합니다.

12표준 입력 읽기

표준 입력을 읽는 대신 process.env와 process.argv에서 값을 꺼내 출력합니다. 파이프로 데이터를 받지 않아도 되는 간단한 도구라면 인자만으로 입력을 대신할 수 있습니다. TypeScript에서 process를 쓰려면 @types/node가 설치되어 있어야 하며, 없으면 컴파일 단계에서 이름을 찾지 못합니다.

TypeScript code

// 표준 입력 읽기
console.log({ env: process.env.NODE_ENV ?? "development", args: process.argv.slice(2) });
알아두면 좋은 점

표준 입력이 필요하면 process.stdin을 for await...of로 돌 수 있고, 파이프가 연결되지 않으면 그대로 대기한다는 점을 감안해야 합니다.

자주 하는 실수

@types/node 없이 process를 쓰면 이름을 찾지 못해 컴파일이 실패하고, any로 선언해 우회하면 argv 오타까지 통과합니다.

13설정 로드

설정 파일 자리에 package.json을 넣어 동기로 읽고 앞 20자를 확인합니다. 애플리케이션이 뜨기 전 한 번만 읽는 설정 로딩은 동기 호출이 오히려 코드가 단순합니다. 다만 읽어 온 값은 string일 뿐이라, JSON.parse를 거쳐도 반환 타입은 any여서 그 뒤의 타입 검사는 사라집니다.

TypeScript code

// 설정 로드
import { readFileSync } from "node:fs";
console.log(readFileSync("package.json", "utf8").slice(0, 20));
알아두면 좋은 점

설정은 시작 시 한 번 읽어 검증까지 마친 객체로 만들어 두면, 이후 코드가 값의 존재 여부를 매번 확인하지 않아도 됩니다.

자주 하는 실수

파싱 결과를 as Config로 단언하면 파일이 비었거나 필드가 빠져도 통과해, 한참 뒤 속성 접근 지점에서야 문제가 터집니다.

14직렬화

이 코드가 파일에 넘기는 값은 이미 완성된 문자열 "hello\n"입니다. 객체 상태를 저장하려면 이 자리에 직렬화 결과를 넣어야 합니다. writeFileSync는 문자열이나 Buffer만 받으므로 객체를 그대로 주면 의도치 않게 [object Object]가 기록될 수 있습니다.

TypeScript code

// 직렬화
import { writeFileSync } from "node:fs";
writeFileSync("sample-14.txt", "hello\n", "utf8");
알아두면 좋은 점

JSON.stringify(value, null, 2)처럼 들여쓰기를 주면 사람이 읽기 좋고, 버전 관리에서 바뀐 줄만 최소로 남습니다.

자주 하는 실수

undefined와 함수 값은 직렬화에서 통째로 빠지므로, 저장한 뒤 다시 읽으면 필드가 사라진 객체를 받게 됩니다.

15로그 파일 회전 흉내

연도 디렉터리와 app-15.log를 합쳐 회전된 로그 파일이 놓일 자리를 계산합니다. 날짜나 순번으로 파일명을 나누는 규칙은 이렇게 경로 조립부터 분리해 두면 다루기 쉽습니다. 경로만 만들었을 뿐이라 실제로 쓰기 전에는 상위 디렉터리를 mkdirSync로 recursive 생성해야 합니다.

TypeScript code

// 로그 파일 회전 흉내
import path from "node:path";
console.log(path.join("logs", "2026", "app-15.log"));
알아두면 좋은 점

mkdirSync(dir, { recursive: true })는 이미 있을 때도 예외를 내지 않아, 존재 확인 없이 그대로 부를 수 있습니다.

자주 하는 실수

상위 디렉터리를 만들지 않고 쓰기를 시도하면 ENOENT가 나며, 로그 기록 실패가 다시 예외를 일으켜 원래 오류를 덮어 버립니다.

정리하며

  • JSON.parse는 any를 돌려주므로 단언 대신 검증 가드를 통과시킵니다.
  • readFile에 인코딩을 넘기지 않으면 Buffer가 나온다는 점을 전제합니다.
  • 경로는 문자열 이어붙이기 대신 path.join으로 만들어 플랫폼 차이를 흡수합니다.
  • process.env 값은 모두 문자열이거나 없으므로 숫자·불리언은 직접 변환합니다.

더 깊이 들어가고 싶다면 TypeScript 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.