본문으로 건너뛰기
Previous
Next
Bun Shell 완벽 가이드 — 크로스 플랫폼 스크립팅

Bun Shell 완벽 가이드 — 크로스 플랫폼 스크립팅

Bun Shell 완벽 가이드 — 크로스 플랫폼 스크립팅

이 글의 핵심

Bun Shell($)로 크로스 플랫폼 스크립트를 작성하는 방법을 정리했습니다. 파서·인터프리터와 프로세스 생성, 크로스 플랫폼 레이어, 파이프·리다이렉션 구현 관점, 프로덕션 패턴까지 포함해 파이프·리다이렉션, 환경 변수, 글로브, 에러 처리, Bash와의 차이를 다룹니다.

이 글의 핵심

Bun Shell은 Bun 런타임에 내장된 크로스 플랫폼 셸입니다. JavaScript·TypeScript 코드 안에서 Bash와 유사한 문법으로 외부 명령을 실행하고, 파이프·리다이렉션·환경 변수·글로브를 다루며, ResponseBuffer 같은 JS 객체와 입출력을 연결할 수 있습니다. Windows에서도 rimrafcross-env 없이 동일한 스크립트를 유지하려는 팀에 특히 유용합니다.

이 글에서는 $ 템플릿 리터럴의 동작, 파서·인터프리터와 프로세스 생성, 크로스 플랫폼 추상화, 파이프·리다이렉션의 구현 관점, 내장 명령과 PATH 실행, 환경 변수 및 글로브, Node·Bun과의 연동, ShellError.nothrow() 등 에러 처리, 프로덕션 스크립팅 패턴, Bash와의 비교까지 실무 관점에서 정리합니다.


1. Bun Shell이란 무엇인가

Bun Shell은 시스템의 /bin/shcmd.exe를 그대로 호출하는 방식이 아니라, Bun 프로세스 안에서 동작하는 Bash에 가까운 작은 언어(구현은 Zig) 입니다. 그래서 기본적으로 명령 주입(shell injection) 에 대해 문자열 보간을 안전하게 처리하도록 설계되어 있습니다.

핵심 특징은 다음과 같습니다.

  • 크로스 플랫폼: Windows, Linux, macOS에서 동일한 스크립트 패턴을 사용할 수 있습니다. ls, cd, rm 등 자주 쓰는 명령은 내장되어 있고, 나머지는 PATH에서 실행됩니다.
  • Bash 스타일: 리다이렉션(>, 2>, >>, 2>&1 등), 파이프(|), 환경 변수 할당, $(...) 형태의 명령 치환을 지원합니다.
  • JavaScript 상호운용: Response, Blob, ArrayBuffer, Bun.file() 등을 stdin·stdout·stderr와 연결할 수 있습니다.
  • 글로브: **, *, {a,b} 형태의 패턴을 셸 수준에서 처리합니다.

공식 문서와 블로그에서 강조하듯, 이 API는 zx, dax 등 기존 프로젝트의 영향을 받았으며, Bun 생태계 안에서 스크립트 자동화를 단순화하는 것이 목표입니다.


Shell 파서와 인터프리터 내부(구현 개요)

Bun Shell은 운영체제에 끼워 넣은 bash -c 한 줄이 아니라, Bun 바이너리에 포함된 독자적인 셸 언어입니다. 공개 저장소(oven-sh/bun)를 보면 src/shell/ 아래에 파서·인터프리터·내장 명령이 Zig로 모여 있으며, 여기서 말하는 “내부”는 사용자가 보는 Bash 유사 문법실제 실행기(Zig 코드) 사이의 경계를 가리킵니다.

파싱 단계: 문자열에서 실행 계획으로

  1. 소스 텍스트 수집: $ 태그된 템플릿 리터럴은 런타임에 문자열과 보간 조각으로 재구성됩니다. 이때 JS 쪽에서 기본 이스케이프가 적용된 인자는, 셸 파서가 토큰으로 나눌 때 “하나의 인자”로 취급되도록 설계되어 있습니다.
  2. 어휘·구문 분석: 파이프(|), 리다이렉션(>, 2>&1 등), 명령 치환($(...)), KEY=value 선행 할당, 글로브 패턴 등은 전통적인 셸과 비슷한 문법 트리(명령 파이프라인, 리다이렉션 목록, 단어·확장 단계) 로 정리됩니다.
  3. 확장(expansion): 글로브, 변수, 명령 치환은 실행 직전에 단계적으로 풀립니다. 여기서 “Bash와 100% 동일”을 기대하면 안 되는 이유가 생깁니다. 미구현·차이가 나는 것은 대개 이 확장 규칙이나 경로 처리의 엣지 케이스입니다.

인터프리터: 상태 기계와 비동기 I/O

문서와 소스 코멘터리에서 반복되는 관점은, Bun Shell이 단순한 재귀 트리 워커가 아니라 상태 머신 기반으로 짜여 있고, I/O가 걸리는 지점에서 양보(yield)·재개할 수 있게 설계되어 있다는 점입니다. 목적은 명확합니다. 외부 프로세스를 기다리거나 파이프로 데이터를 흘릴 때 메인 JavaScript 스레드를 불필요하게 막지 않는 것입니다.

실무적으로 기억할 포인트는 다음과 같습니다.

  • 파이프라인은 “한 줄”처럼 보여도 내부적으로는 여러 단계의 준비·실행·정리가 있으며, 중간에 실패하면 앞 단계의 리소스 정리 순서가 중요합니다.
  • 명령 치환 $(...) 은 “서브셸 한 번”과 비슷한 중첩된 평가 컨텍스트를 만들며, 여기서도 동일한 파서·인터프리터가 재사용됩니다.
  • 구현 세부는 버전마다 바뀔 수 있으므로, 행동 보장이 필요한 스크립트는 Bun 버전을 고정하고, 중요한 경로는 Bash 스크립트와 동일하게 대상 OS에서 스모크 테스트하는 것이 안전합니다.

프로세스 생성 메커니즘

Bun Shell에서 “명령이 실행된다”는 말은 두 갈래로 나뉩니다. 내장(builtin)으로 처리되는 경우새 프로세스를 띄우는 경우입니다.

내장 명령: 같은 Bun 프로세스 안

cd, echo, ls 등 문서에 나열하는 내장 명령은 별도의 exec가 아니라 Bun 프로세스 내부의 Zig 루틴으로 처리되는 경우가 많습니다. 그래서 Windows에서도 ls 스타일이 통일되고, 시작 비용이 상대적으로 작습니다. 대신 시스템 셸과 1:1로 같은 의미를 보장하지는 않으며, 문서에 적힌 제약(예: 일부 mv 시나리오)처럼 엣지 케이스가 남을 수 있습니다.

외부 명령: PATH 탐색과 자식 프로세스

내장에 없는 이름은 실행 파일 탐색 → spawn에 상응하는 자식 프로세스 생성으로 이어집니다. 인자는 이미 파서·확장 단계에서 문자열 배열 형태로 정리되고, 보안을 위해 템플릿 보간이 기본 이스케이프되는 이유도 이 지점에서 드러납니다. “한 덩어리 인자”로 넘어가야 git이나 node옵션 주입(argument injection) 으로 해석하지 않기 때문입니다.

실행 모델을 한 줄로 요약하면 “파서가 파이프라인과 리다이렉션을 해석하고, 각 단계마다 builtin이면 인프로세스, 아니면 서브프로세스” 입니다. 공개 코드베이스에서는 Cmd 같은 상태 객체가 인자 확장·리다이렉션 적용·비동기 대기를 묶는 축으로 등장합니다. 세부 타입 이름은 버전에 따라 달라질 수 있으나, “명령 한 번 = 상태 머신 한 번” 이라는 그림은 문서를 읽을 때 방향을 잡는 데 도움이 됩니다.

JavaScript 객체와의 연결

Response, Buffer, Bun.file() 등은 파일 디스크립터 대신 JS 런타임이 이해하는 엔드포인트로 매핑됩니다. 즉, 리다이렉션이 “파일 열기” 수준이 아니라 스트림 브리지 수준이 될 수 있어, 임시 파일 없이 메모리 파이프라인을 구성하기 쉽습니다. 이는 다음 절의 파이프·리다이렉션과 직결됩니다.


크로스 플랫폼 호환 레이어

Windows와 Unix 계열은 프로세스 생성 API, 경로 규칙, 기본 셸이 모두 다릅니다. Bun Shell이 “크로스 플랫폼”이라고 불리는 이유는, 사용자 스크립트가 OS별 cmd.exe/sh 차이를 직접 흡수하지 않고, 단일 인터프리터가 같은 문법을 해석하기 때문입니다.

무엇이 추상화되나

  • 명령 체감: ls, rm 등을 내장으로 제공해 PowerShell 사용자에게도 POSIX 스타일 워크플로를 맞춥니다.
  • 경로와 실행 파일: 외부 도구는 결국 각 OS의 실행 파일이어야 하므로, PATH에 등록된 이름이 플랫폼마다 다를 수 있습니다. 예를 들어 Windows 전용 .exe와 Unix 전용 스크립트를 갈아끼우는 패턴은 여전히 필요할 수 있습니다.
  • 줄바꿈·인코딩: 스크립트 파일은 팀 규칙으로 LF 고정, CI에서 core.autocrlf 등을 명시하는 것이 혼선을 줄입니다.

한계와 실무 대응

추상화 레이어는 셸 언어와 내장 명령에 가장 강하고, 네이티브 바이너리의 옵션·출력 형식까지 통일해 주지는 않습니다. 크로스 플랫폼 배포 스크립트를 쓸 때는 (1) Bun 버전 고정, (2) 세 OS에서 동일 테스트, (3) 외부 도구는 별도 설치 문서까지 세트로 두는 것이 일반적인 프로덕션 패턴입니다.


2. 시작하기: $ 템플릿 리터럴

가장 기본적인 사용법은 bun 패키지에서 $를 가져와 태그된 템플릿 리터럴로 명령을 실행하는 것입니다.

import { $ } from "bun";

await $`echo "Hello, Bun Shell!"`;

기본적으로 표준 출력으로 결과가 출력됩니다. 출력을 숨기려면 .quiet()를 붙입니다.

await $`echo "quiet"`.quiet();

명령의 결과를 문자열로 받으려면 .text()를 사용합니다. .text()는 내부적으로 출력을 조용히 처리하는 동작과 맞물리도록 설계되어 있어, 캡처 목적일 때 편합니다.

const welcome = await $`echo "Hello World!"`.text();
console.log(welcome); // "Hello World!\n"

터미널에 출력하지 않고 stdout/stderrBuffer로 받으려면 .quiet()를 붙인 뒤 await 결과를 구조 분해합니다.

const { stdout, stderr } = await $`echo "Hello!"`.quiet();
console.log(stdout); // Buffer

정리하면, 화면에 찍을지·문자열로 받을지·버퍼로 다룰지를 메서드 체인으로 선택하는 패턴이 Bun Shell의 기본 사용 흐름입니다.


3. 문법과 명령어

3.1 내장 명령과 PATH

크로스 플랫폼 호환을 위해 Bun Shell은 일부 명령을 내장(builtin) 으로 제공합니다. 예를 들어 cd, ls, rm, echo, pwd, cat, touch, mkdir, which, mv, exit, true, false, yes, seq, dirname, basename 등이 여기에 해당합니다. 내장에 없는 실행 파일은 운영체제의 PATH를 통해 찾아 실행합니다.

내장 구현과 완전히 동일하지 않을 수 있으므로, 예를 들어 mv는 문서상 교차 디바이스 이동 등 일부 시나리오가 미구현일 수 있습니다. 배포 스크립트를 작성할 때는 대상 OS에서 한 번씩 검증하는 것이 안전합니다.

3.2 명령 치환 $(...)

다른 명령의 출력을 현재 명령줄에 끼워 넣을 때 Bash와 같이 $(...) 구문을 사용합니다.

await $`echo "현재 커밋: $(git rev-parse HEAD)"`;

여러 줄 스크립트에서 셸 변수에 결과를 담아 이어서 쓰는 패턴도 가능합니다.

await $`
  REV=$(git rev-parse HEAD)
  echo "빌드 태그: $REV"
`;

주의할 점은, JavaScript 템플릿 리터럴의 백틱 명령 치환은 Bun Shell에서 기대한 대로 동작하지 않을 수 있어, 공식 문서에서는 $(...) 형태를 사용할 것을 권장합니다.

3.3 $.braces$.escape

중괄호 확장(brace expansion)을 미리 문자열 배열로 펼쳐 보고 싶을 때 $.braces를 쓸 수 있습니다.

import { $ } from "bun";

const expanded = await $.braces(`echo {1,2,3}`);
// => ["echo 1", "echo 2", "echo 3"]

사용자 입력이나 동적 문자열을 셸에 넣기 전에 이스케이프 규칙을 맞추고 싶다면 $.escape를 사용합니다. 반대로 의도적으로 이스케이프를 건너뛰려면 { raw: '...' } 형태로 감쌀 수 있으나, 그 경우 셸이 문자열을 어떻게 해석하는지 직접 책임져야 합니다.


4. 파이프와 리다이렉션

4.1 파이프 |

한 명령의 표준 출력을 다음 명령의 표준 입력으로 넘기는 방식은 Bash와 동일합니다.

const wordCount = await $`echo "Hello World!" | wc -w`.text();

fetch로 받은 Response를 stdin으로 넣고 파이프로 이어 붙이는 예는 공식 문서의 패턴 그대로 실무에서도 유용합니다.

const response = await fetch("https://example.com");
const byteLen = await $`cat < ${response} | wc -c`.text();

4.2 리다이렉션 연산자

Bun Shell이 지원하는 대표 연산자는 다음과 같습니다.

연산자의미
<표준 입력 리다이렉션
>, 1>표준 출력(덮어쓰기)
2>표준 에러
&>표준 출력과 표준 에러 동시에
>>, 1>>표준 출력 추가
2>>표준 에러 추가
&>>둘 다 파일 끝에 추가
2>&1stderr를 stdout으로 합침
1>&2stdout을 stderr로 보냄

파일뿐 아니라 JavaScript 객체로도 리다이렉션할 수 있습니다. 예를 들어 Buffer에 stdout을 쓰거나, Response 본문을 stdin으로 읽는 식입니다.

const buf = Buffer.alloc(256);
await $`echo "Hello" > ${buf}`;

const res = new Response("body text");
const out = await $`cat < ${res}`.text();

이 패턴은 임시 파일을 만들지 않고 메모리 상에서 데이터를 주고받을 때 특히 깔끔합니다.

4.3 구현 관점: 파이프, FD, 스트림 브리지

문법상 파이프(|)는 이전 명령의 표준 출력(파일 디스크립터 1)다음 명령의 표준 입력(0) 을 잇는 연산입니다. 운영체제 수준에서는 동일한 이름의 파이프(fd 쌍)를 만들고, 왼쪽 프로세스의 쓰기 끝과 오른쪽 프로세스의 읽기 끝을 연결하는 그림이 됩니다. Bun Shell은 이 연결을 여러 외부 프로세스가 동시에 살아 있는 상태에서 처리해야 하므로, 앞서 말한 것처럼 블로킹을 피하는 실행 모델(상태 머신·비동기 I/O)과 맞물립니다.

리다이렉션은 “어느 FD를 어디로 보낼지”를 바꾸는 연산입니다. > filestdout을 파일로 바꾸고, 2> filestderr(2) 를 분리합니다. 2>&1stderr를 stdout과 같은 싱크로 합치는 FD 재배치이며, 이후 파이프나 | tee 같은 패턴과 결합할 때 Bash와 동일한 사고방식으로 읽을 수 있습니다. &> 계열은 두 스트림을 동일 대상에 묶는 축약 표기로 이해하면 됩니다.

Bun Shell의 특이점은 파일만이 아니라 JS 객체를 대상으로 할 수 있다는 점입니다. 이는 단순히 “파일 경로 문자열 대신 객체”를 받는 수준이 아니라, 런타임이 바이트 스트림 엔드포인트로 변환해 프로세스의 fd와 연결한다는 점에서 구현 난이도가 높습니다. 그래서 임시 파일을 만들지 않고도 BufferResponse를 파이프라인에 끼워 넣을 수 있고, 반대로 대용량 스트림을 다룰 때는 메모리 사용량과 백프레셔(읽는 쪽이 느릴 때의 정체)를 머릿속에 두는 것이 좋습니다. 이런 경우에는 파일로 스풀(spool)하거나, 단계를 나누어 짧은 파이프라인으로 쪼개는 편이 디버깅에 유리합니다.


5. 환경 변수와 글로빙

5.1 환경 변수

Bash처럼 명령 앞에 KEY=value 형태로 한 줄짜리 환경을 줄 수 있습니다.

await $`NODE_ENV=production bun -e 'console.log(process.env.NODE_ENV)'`;

템플릿 안에서 ${variable}으로 값을 끼워 넣으면 기본적으로 이스케이프되어, 세미콜론 등이 포함된 문자열이 별도의 명령으로 쪼개지지 않습니다. 이는 악의적 입력에 대한 1차 방어선입니다.

특정 호출에만 환경을 바꾸려면 .env({ ... })를 사용합니다.

await $`echo $FOO`.env({ ...process.env, FOO: "bar" });

모든 이후 $ 호출에 기본 환경을 고정하려면 $.env(...)를 호출하고, 기본값으로 되돌리려면 인자 없이 $.env()를 호출하는 방식으로 문서에 설명되어 있습니다.

작업 디렉터리는 .cwd("/path")로 바꿀 수 있으며, 마찬가지로 $.cwd로 전역 기본값을 설정할 수 있습니다.

5.2 글로빙(Glob)

Bun Shell은 **, *, {a,b} 같은 글로브 패턴을 네이티브로 처리합니다. 여러 파일을 한 번에 넘기거나 소스 트리 전체를 대상으로 명령을 돌릴 때 Bash 스크립트와 비슷한 표현을 유지할 수 있습니다. 다만 패턴이 매우 크거나 예외 케이스가 많다면, Node 쪽에서 glob 라이브러리로 목록을 만든 뒤 인자로 넘기는 편이 디버깅에 유리할 때도 있습니다.


6. Node.js·Bun 스크립트와 통합

6.1 Bun에서의 표준 패턴

$import { $ } from "bun" 으로만 제공됩니다. 즉 이 API는 Bun 런타임을 전제로 합니다. 자동화 스크립트·CLI·빌드 도구를 Bun으로 실행하는 프로젝트에서는 package.jsonscriptsbun run tools/deploy.ts처럼 두고, 그 안에서 $를 사용하면 됩니다.

6.2 Node.js와의 관계

Node.js 공식 런타임에는 동일한 $ API가 없습니다. Node 생태계에서는 node:child_processspawn/execFile, 또는 위에서 언급한 zx 같은 라이브러리를 쓰는 것이 일반적입니다. 팀 표준을 Bun으로 통일할 수 있다면 Shell 스크립트 대신 TypeScript 한 벌로 크로스 플랫폼 자동화를 가져갈 수 있다는 점이 Bun Shell의 실무적 이점입니다.

반대로 기존 npm 스크립트 안에서 한 줄만 Bun Shell을 쓰고 싶다면 bun -e '...'로 감싸 호출할 수는 있으나, 유지보수 측면에서는 전용 .ts 파일로 분리하는 편이 낫습니다.

6.3 .sh 로더와 배포

확장자 .sh 파일을 bun ./script.sh로 실행하면 Bun Shell이 해석합니다. Shebang 없이도 Windows PowerShell에서 동일 파일을 실행할 수 있다는 점이 문서에서 강조됩니다. 다만 운영 환경에 Bun이 설치되어 있어야 하므로, CI 이미지와 개발자 문서에 Bun 버전 고정을 명시하는 것이 좋습니다.


7. 에러 처리

기본적으로 자식 프로세스가 0이 아닌 종료 코드로 끝나면 예외가 발생합니다. 타입은 문서상 ShellError로, exitCode, stdout, stderr 등을 확인할 수 있습니다.

import { $ } from "bun";

try {
  await $`명령이-실패할-수-있음`.text();
} catch (err: any) {
  console.error("코드:", err.exitCode);
  console.error(err.stdout?.toString());
  console.error(err.stderr?.toString());
}

예외를 던지지 않고 직접 분기하려면 .nothrow()를 사용합니다.

const { stdout, stderr, exitCode } = await $`명령`.nothrow().quiet();
if (exitCode !== 0) {
  // 복구 로직
}

전역적으로 기본 동작을 바꾸려면 $.nothrow() 또는 $.throws(false) / $.throws(true)를 사용합니다. CI 파이프라인처럼 실패 시 즉시 중단이 기대되면 기본(throw)을 유지하고, 로그만 수집하는 도구에서는 .nothrow()를 선택하는 식으로 역할을 나누면 됩니다.

출력을 읽는 편의 메서드로는 .text(), .json(), .lines(), .blob() 등이 있어, 파이프라인 결과를 바로 파싱하거나 줄 단위로 스트리밍 처리할 수 있습니다.


8. Bash vs Bun Shell

구분Bash(및 일반 POSIX sh)Bun Shell
실행 환경OS에 설치된 셸 프로세스Bun 프로세스 내장 인터프리터
크로스 플랫폼Windows는 WSL/Git Bash 등 별도 대응이 필요한 경우가 많음Windows 포함 동일 문법 지향
보안 모델문자열 조합 실수 시 주입 위험보간 문자열 기본 이스케이프
JS 연동문자열로만 주고받기 쉽다Buffer, Response 등 직접 연결
완전한 Bash 호환표준일부 Bash 기능은 미구현·차이 가능
동시성일반적으로 순차 파이프문서상 연산이 동시에 처리되는 부분이 있음(구현 세부)

언제 Bash를 쓰고 언제 Bun Shell을 쓸까에 대한 실무 기준은 다음과 같습니다.

  • 서버나 컨테이너 진입점이 이미 bash 이고, 운영팀이 셸만 허용한다면 Bash를 유지합니다.
  • 프론트엔드·풀스택 모노레포에서 TypeScript로 빌드·배포·검증을 한 언어로 묶고 싶다면 Bun Shell이 유리합니다.
  • Windows 개발자가 많고 동일한 npm 스크립트를 깨지 않게 하려면 Bun Shell + 내장 명령 조합을 검토합니다.

9. 보안을 끝까지 이해하기

기본 보간은 안전하지만, bash -c처럼 새 셸을 명시적으로 띄우면 그 이후의 해석은 Bun이 막을 수 없습니다. 또한 외부 프로그램(git 등)은 인자 하나를 자신만의 옵션으로 해석할 수 있으므로, --upload-pack= 같은 악성 플래그 형태의 입력은 애플리케이션 레벨에서 검증해야 합니다. 공식 문서의 “argument injection” 예시는 이 차이를 잘 보여 줍니다.


프로덕션 셸 스크립팅 패턴

Bun Shell은 로컬 편의를 넘어 CI·릴리스·운영 자동화에 들어갈 때, “스크립트가 아니라 소프트웨어”처럼 다루는 것이 안전합니다.

런타임과 재현성

  • package.jsonengines 또는 팀 표준 도구(예: mise, asdf)로 Bun 버전을 고정합니다. 셸 파서·내장 명령은 바이너리 버전에 따라 미세하게 달라질 수 있습니다.
  • 동일한 스크립트를 Windows·macOS·Linux에서 실행한다면 CI 매트릭스에 세 OS를 넣고, 최소한 스모크 테스트(핵심 $ 호출 한 번)을 돌립니다.
  • 의존 외부 도구(git, docker, 클라우드 CLI)는 Bun과 별개이므로, README에 최소 버전과 설치 링크를 적습니다.

실패 모델과 로그

  • 기본 동작은 비영 종료 시 예외입니다. CI에서는 이것이 “첫 실패에서 멈춤”과 같아 유리합니다. 반면 진단 스크립트처럼 “실패해도 계속 시도”가 필요하면 .nothrow()로 분기하고, 성공한 단계만 요약 로그로 남깁니다.
  • 민감한 값console.log에 직접 붙이지 말고, CI 시크릿은 환경 변수로만 주입하고 출력 전에 마스킹합니다. $ 문자열 안에 토큰을 직접 이어 붙이기보다, 가능하면 .env({ ... })로 주입해 실수로 인한 유출을 줄입니다.

구조·멱등·롤백

  • 스크립트를 짧은 함수로 나누고, 각 단계는 입력·출력 경로만 명확히 합니다. “한 줄에 모든 것”은 재현이 어렵습니다.
  • 배포·마이그레이션은 여러 번 실행해도 안전한지(멱등성) 를 검토합니다. 동일한 $ 호출을 재실행했을 때 기대 결과가 같아야 합니다.
  • 임시 디렉터리Bun.write/Bun.file과 조합하거나 OS의 표준 임시 경로 규칙을 따르고, 실패 시 부분 산출물 정리try/finally로 보장합니다.

운영 관점의 절충

  • 서버의 공식 진입점이 Bash이고 감사 규칙이 셸만 허용한다면, Bun Shell은 빌드·개발 자동화에 두고 운영은 Bash를 유지하는 이중 레이어가 흔한 절충안입니다.
  • 장기적으로는 스크립트 경로·버전·실패 정책을 문서화한 “작은 런북”을 두면, 팀 합류 시 온보딩 비용이 줄어듭니다.

10. 정리

Bun Shell은 크로스 플랫폼 스크립팅을 TypeScript 한가운데로 가져오면서, Bash 사용자에게 익숙한 파이프·리다이렉션·환경 변수·명령 치환을 제공합니다. 내부적으로는 Zig 기반 파서·인터프리터가 내장 명령과 자식 프로세스를 나누어 실행하고, 파이프·리다이렉션은 FD와 스트림을 엮는 작업으로 이해하면 동작이 예측 가능해집니다. $ 템플릿, .text()·.lines() 등 출력 API, .nothrow()ShellError로 구성된 에러 모델만 익혀도 대부분의 자동화 시나리오를 커버할 수 있으며, 프로덕션에서는 버전 고정·OS 매트릭스 검증·비밀 관리·실패 정책 합의를 함께 두는 것이 좋습니다. Bash와 100% 동일하지는 않으므로, 중요한 배포 스크립트는 대상 OS에서 반드시 검증하고, 보안 민감한 입력은 외부 명령의 옵션 규칙까지 함께 고려하는 것이 좋습니다.


자주 묻는 질문

Q. Node.js만 설치된 환경에서도 $를 쓸 수 있나요?
A. 아니요. import { $ } from "bun"은 Bun 전용입니다. Node에서는 child_process나 zx 등을 사용해야 합니다.

Q. Windows에서도 ls, rm 같은 명령이 동작하나요?
A. Bun Shell이 내장 명령으로 제공하므로, 문서에 설명된 범위에서는 PowerShell과 별도로 동일한 스타일로 사용할 수 있습니다. 다만 PATH에 올라온 외부 도구는 각 OS별 실행 파일이어야 합니다.

Q. 비영(非零) 종료 코드가 나와도 예외를 피하고 싶습니다.
A. 해당 호출에 .nothrow()를 붙이거나, 전역으로 $.nothrow()를 설정한 뒤 exitCode를 검사하세요.

Q. Bun Shell 내부는 어떻게 구성되어 있나요?
A. 공개 소스 기준 Zig로 작성된 파서·인터프리터가 있으며, 내장 명령은 Bun 프로세스 안에서, 외부 명령은 자식 프로세스로 실행되는 이중 구조입니다.

Q. 프로덕션에서 가장 먼저 챙길 것은?
A. Bun 버전 고정, CI에서 OS 매트릭스 검증, 비밀·토큰의 로그 유출 방지, 실패 시 동작(throw 대비 .nothrow) 합의입니다.

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「Bun Shell 완벽 가이드 — 크로스 플랫폼 스크립팅」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「Bun Shell 완벽 가이드 — 크로스 플랫폼 스크립팅」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Bun, Shell, Scripting, Cross-platform, CLI 등으로 검색하시면 이 글이 도움이 됩니다.