이번 포스팅에서는 Bruno API 클라이언트에 대해서 정리하고자 한다. API 테스트 도구는 오랫동안 Postman의 독무대였다. 그런데 요청 정의가 클라우드 워크스페이스 안에만 존재하는 구조는 코드 리뷰와 버전 관리에 익숙한 백엔드 개발자에게 늘 어색한 지점이었다. Bruno API 클라이언트는 이 구조를 뒤집는다. 모든 요청을 로컬 디렉터리의 평문 파일로 저장하고, 그 디렉터리를 그대로 Git 저장소로 다룬다. 설치와 컬렉션 구조부터 CI 연동, Postman에서 넘어올 때 챙겨야 할 항목까지 실무에서 마주치는 순서대로 정리한다.
Bruno API 클라이언트가 갑자기 눈에 띄기 시작한 이유

Bruno API 클라이언트가 2026년 들어 주목받는 직접적 계기는 2026년 3월 1일부터 시행된 Postman 무료 플랜 축소다. 무료 플랜이 1인 사용으로 제한되면서 2명 이상인 팀은 유료 Team 플랜으로 올라가야 했다. 여기에 요청 데이터가 외부 클라우드에 저장되는 구조 자체를 부담스러워하던 조직의 수요가 겹쳤다.
Bruno API 클라이언트는 API 요청을 클라우드가 아닌 로컬 평문 파일로 저장하고 Git으로 버전 관리하는 오픈소스 API 클라이언트다. 계정 로그인이나 동기화 없이 완전 오프라인으로 동작하며, 요청 데이터가 개발자의 디스크 밖으로 나가지 않는다. GitHub 저장소는 스타 45.8k를 기록 중이고 라이선스는 MIT, 최신 안정 버전은 2026년 7월 20일 릴리스된 v3.5.3이다.
금융이나 의료처럼 데이터 반출 규정이 엄격한 도메인에서는 요청 바디에 포함된 샘플 데이터 하나도 컴플라이언스 검토 대상이 된다. 사내 정책상 외부 SaaS 도구 반입이 까다로운 조직에서 MIT 라이선스와 오프라인 동작이라는 조합은 그 자체로 도입 명분이 된다.
체감으로 오는 차이는 리소스 사용량이다. 공식 벤치마크가 공개된 항목은 아니지만, 두 앱을 동시에 띄워두고 작업해 보면 메모리 점유와 창이 뜨는 속도에서 격차가 분명하다. Bruno는 Electron 앱치고 가벼운 축에 속하고, 하루에도 몇 번씩 켜고 끄는 도구에서 이 차이는 생각보다 크게 느껴진다.
Bruno 설치, 왜 패키지를 두 개 깔아야 하나
Bruno API 클라이언트 설치는 각 OS의 기본 패키지 매니저 한 줄로 끝난다. 다만 GUI 앱과 CI에서 쓰는 bru CLI는 서로 다른 패키지다. 둘 다 필요하면 각각 설치해야 한다. GUI만 깔아놓고 터미널에서 bru 명령을 찾지 못해 당황하는 경우가 흔하다.
# macOS (Homebrew에는 formula가 아닌 cask로 등록돼 있다)
brew install --cask bruno
# Windows
choco install bruno
# Linux
snap install bruno
# CI에서 쓸 CLI (별도 설치)
npm install -g @usebruno/cli
bru --versionShellScript위 명령은 데스크톱 앱과 커맨드라인 러너를 각각 설치한다. 앞의 세 줄은 GUI 앱을 설치하는 명령이고, 마지막 두 줄의 @usebruno/cli는 컬렉션을 헤드리스로 실행하는 별도 패키지다. macOS에서 --cask를 생략해도 Homebrew의 폴백 덕분에 대개 설치되지만, CI 스크립트에서는 명시하는 편이 안전하다. 두 패키지는 버전이 독립적으로 올라간다. CI에서 GUI와 다른 동작을 만났다면 bru --version부터 확인하는 것이 순서다.
설치 후 앱을 열고 컬렉션을 새로 만들면 곧바로 로컬 디렉터리를 지정하라는 요구를 받는다. 이 지점이 Postman과 갈리는 결정적 차이다. Postman은 컬렉션을 만들면 클라우드 워크스페이스 안에 레코드가 생기지만, Bruno API 클라이언트는 파일시스템 위의 실제 폴더를 요구한다. 그리고 그 폴더는 그대로 git init의 대상이 될 수 있다.
정리하면 컬렉션 생성 = 디렉터리 생성이다. 프로젝트 저장소 안에 api-collection 같은 디렉터리를 만들고 그 안에 컬렉션을 두면, API 요청 정의가 애플리케이션 코드와 같은 저장소, 같은 브랜치, 같은 PR 안에서 움직인다. 더 자세한 설치 옵션과 지원 플랫폼은 Bruno 공식 문서에 정리돼 있다.
Bruno 컬렉션은 왜 그냥 폴더인가

컬렉션이 파일이라는 말은 곧 API 명세 변경이 diff로 보인다는 뜻이다. Bruno API 클라이언트의 컬렉션은 디렉터리 구조 그 자체이며, 폴더는 요청 그룹이 되고 파일 하나가 요청 하나가 된다. 별도의 export/import 절차 없이 파일 탐색기와 에디터로 열어볼 수 있다.
my-collection/
├── opencollection.yml # 컬렉션 루트 설정
├── environments/
│ └── development.yml # 환경 변수
├── users/
│ ├── folder.yml # 폴더 단위 설정
│ ├── create-user.yml
│ ├── get-user.yml
│ └── delete-user.yml
└── orders/
└── create-order.ymlPlaintext위 구조는 사용자 관련 요청 네 개와 주문 요청 하나를 담은 컬렉션이다. 루트의 opencollection.yml은 컬렉션 전체에 적용되는 설정을, 각 폴더의 folder.yml은 그 폴더 하위 요청에만 적용되는 설정을 담는다. 설정이 컬렉션에서 폴더로, 폴더에서 요청으로 내려오며 상속되므로, 공통 인증은 루트에 한 번만 적어두면 개별 요청 파일이 훨씬 가벼워진다.
포맷 이야기를 짚고 넘어갈 필요가 있다. Bruno 3.0.0부터 OpenCollection 기반의 YAML 포맷을 지원하기 시작했고, v3.1.0부터는 신규로 만들거나 임포트하는 컬렉션의 기본 포맷이 YAML로 전환됐다. 전환 배경과 설계 의도는 공식 블로그의 v3.1 YAML 기본화 글에 정리돼 있다.
기존 .bru 포맷이 사라지는 것은 아니다. 두 포맷은 계속 지원되며 한 컬렉션 안에 공존할 수도 있다. 마이그레이션은 export 후 re-import 방식으로 무손실 진행된다. 성능 면에서는 공식 벤치마크상 YAML 파싱이 BRU 대비 4~5배 빠르고, 요청 수백 개짜리 대형 컬렉션에서 확장성이 더 좋다고 명시돼 있다. 포맷 스펙 자체는 OpenCollection YAML 개요 문서에서 확인할 수 있다.
실무에서 이 구조가 주는 이득은 두 갈래다. 코드 리뷰에서 API 스펙 변경이 diff로 드러나고, 누가 언제 어떤 엔드포인트의 필수 파라미터를 바꿨는지가 커밋 히스토리에 남는다. 그리고 브랜치별로 다른 요청 정의를 가질 수 있다. 기능 브랜치에서 새 엔드포인트를 추가하면 그 브랜치를 체크아웃한 사람만 해당 요청을 본다. 컬렉션이 코드와 같은 생애주기를 갖게 되는 셈이다.
Bruno 요청 파일 하나를 해부해 보면 보이는 것들
Bruno의 YAML 요청 파일은 info, http, runtime, settings 네 개의 최상위 키로 구성된다. 메타데이터, 실제 HTTP 요청, 스크립트, 동작 옵션이 각각 분리돼 있어서 파일만 봐도 무엇을 어디에 적어야 하는지 짐작할 수 있다. 아래는 사용자 생성 요청 하나의 전체 모습이다.
info:
name: Create User
type: http
seq: 1
http:
method: POST
url: https://api.example.com/users
body:
type: json
data: |-
{
"name": "John Doe",
"email": "john@example.com"
}
auth: inherit
runtime:
scripts:
- type: tests
code: |-
test("should return 201", function() {
expect(res.status).to.equal(201);
});
settings:
encodeUrl: trueYAML이 파일은 POST 요청 하나와 그에 딸린 테스트 코드를 함께 담고 있다. info.seq는 컬렉션 러너가 요청을 실행하는 순서를 정하는 값이고, http.auth: inherit는 상위 폴더나 컬렉션 루트에 정의된 인증 설정을 그대로 물려받겠다는 선언이다. 토큰 갱신 정책이 바뀌어도 루트 파일 한 곳만 고치면 되므로 요청이 수십 개로 늘어날수록 이 상속 구조의 효용이 커진다.
주목할 부분은 runtime.scripts다. 테스트 코드가 별도 파일이 아니라 요청 파일 안에 인라인으로 들어간다. type: tests로 지정한 블록은 응답을 받은 뒤 실행되며, 이 안에서 Chai 스타일의 expect 문법을 그대로 쓸 수 있다. 요청과 검증이 한 파일에 있으니 리뷰어가 파일 하나만 열어도 이 엔드포인트가 무엇을 기대하는지 파악한다. settings에는 encodeUrl처럼 요청 단위 동작을 조정하는 옵션이 들어간다. 공식 샘플은 모두 encodeUrl: true로 되어 있으며, URL 인코딩 처리를 요청마다 켜고 끌 수 있다.
기존 .bru 포맷을 쓰던 컬렉션이라면 문법이 다르게 보인다. .bru는 get {}, headers {}, body {}, tests {} 같은 블록 문법을 쓰며, 블록 종류는 키-값을 담는 딕셔너리 블록, 자유 텍스트를 담는 텍스트 블록, 목록을 담는 배열 블록 세 가지로 나뉜다. 같은 정보를 다른 표기로 적을 뿐이므로 개념을 새로 배울 필요는 없다.
Bruno 환경 변수로 로컬과 스테이징을 스위치 하나로 오가기

Bruno의 환경 변수는 environments 디렉터리 아래에 환경명으로 파일을 하나씩 두는 방식으로 관리된다. 요청 본문에서는 {{host}} 형태로 참조하며 URL, 헤더, 바디 어디에서든 동일하게 동작한다. 앱 상단의 환경 선택 드롭다운을 바꾸면 모든 요청의 대상이 한 번에 전환된다.
# environments/local.yml
name: local
variables:
- name: host
value: http://localhost:8787
- name: apiVersion
value: v1YAML이 파일은 로컬 개발 환경을 정의한다. YAML 포맷에서 환경 변수는 variables 아래에 name/value 쌍의 배열로 들어간다. 레거시 .bru 포맷의 vars { host: ... } 블록 문법과 키 이름부터 다르므로, 두 포맷 예제를 섞어 쓰다 파싱 오류를 만나는 경우가 잦다. 요청 URL을 {{host}}/{{apiVersion}}/users처럼 적어두면 환경만 바꿔서 로컬과 스테이징, 운영을 오갈 수 있다. 파일명은 local.yml, staging.yml, ci.yml처럼 목적이 드러나게 짓는 편이 관리에 수월하다. 변수 참조 문법과 우선순위는 Variables 공식 문서에 정리돼 있다.
여기서 Postman에서 넘어오는 사람이 반드시 알아야 할 함정이 하나 있다. 임포트 과정에서 모든 변수값이 문자열로 저장된다. 숫자 5000은 "5000"이 되고 불리언 true는 "true"가 된다. 이 값을 스크립트에서 숫자로 비교하거나 조건식에 그대로 넣으면 의도와 다른 결과가 나온다. 임포트 직후에는 Number()로 감싸거나 명시적으로 비교하도록 손봐야 한다.
Git 저장소에 환경 파일을 올릴 때는 비밀값 분리가 관건이다. 컬렉션 전체를 커밋하는 것이 이 도구의 핵심 이점인데, API 키가 들어간 환경 파일까지 그대로 올리면 이점이 사고로 바뀐다. 실무에서는 호스트나 API 버전처럼 팀 공용인 값만 담은 환경 파일을 커밋하고, 토큰이나 시크릿은 별도 파일로 분리해 .gitignore에 넣거나 프로세스 환경 변수로 주입하는 방식을 쓴다. CI에서는 bru run --env-var KEY=VALUE 형태로 실행 시점에 값을 덮어쓰는 방법이 가장 깔끔하다.
로그인 토큰을 다음 요청에 넘기는 법, Bruno 요청 체이닝

Bruno에서 요청 체이닝은 앞 요청의 응답에서 값을 꺼내 변수에 저장하고, 뒤 요청에서 그 변수를 쓰는 방식으로 구현한다. 로그인 → 토큰 저장 → 인증이 필요한 API 호출이라는 가장 흔한 시나리오가 이 패턴 하나로 해결된다. 스크립트는 요청 파일의 post-response 영역에 들어간다.
// post-response script: 로그인 응답에서 토큰을 꺼내 변수로 저장
if (res.status === 200) {
bru.setVar("authToken", res.body.token);
console.log("Login successful, token saved");
}JavaScript이 스크립트는 로그인 요청의 응답을 받은 직후 실행된다. 스크립트 안에서 응답에 접근하는 전역 객체는 res이고, res.status와 res.body로 상태 코드와 파싱된 바디를 바로 꺼낼 수 있다. Postman의 response 같은 이름은 Bruno에 없으므로 그대로 쓰면 ReferenceError가 난다. 상태 코드 조건을 두는 이유는 로그인 실패 응답에는 토큰 필드가 없어 undefined가 저장되고, 그 상태로 다음 요청이 진행되면 원인 파악이 어려운 401이 연쇄로 터지기 때문이다.
저장한 토큰을 다음 요청에서 쓰는 방법은 두 가지다. 요청 파일의 헤더에서 변수로 참조하는 쪽이 가장 흔하다.
# 요청 파일에서 저장된 변수를 헤더로 참조
http:
method: GET
url: "{{host}}/v1/me"
headers:
- name: Authorization
value: "Bearer {{authToken}}"YAMLYAML 포맷에서 헤더는 http.headers 아래 name/value 배열로 들어가고, 값 안에서 {{authToken}}처럼 변수를 그대로 보간할 수 있다. 스크립트를 한 줄도 쓰지 않고 토큰이 붙는다는 점이 장점이다. 헤더 값을 조건에 따라 바꿔야 한다면 pre-request 스크립트에서 req.setHeader('Authorization', 'Bearer ' + bru.getVar('authToken'))으로 직접 주입하면 된다.
여기서 헤더 상속과 auth 상속을 헷갈리지 않는 것이 중요하다. 앞서 나온 auth: inherit는 상위 폴더나 컬렉션에 정의된 auth 설정 블록(Bearer, Basic, OAuth2 등)을 물려받는 장치이지, 손으로 적은 Authorization 헤더를 물려받는 기능이 아니다. 인증을 한 곳에서 관리하려면 컬렉션 루트에 Bearer auth를 정의하고 개별 요청에서 auth: inherit를 쓰는 편이 깔끔하다.
// 응답 바디의 상태값에 따라 다음에 실행할 요청을 분기
const status = res.body.status;
if (status === 'active') {
bru.runner.setNextRequest("process-active-user");
} else if (status === 'inactive') {
bru.runner.setNextRequest("handle-inactive-user");
} else {
bru.runner.stopExecution(); // 컬렉션 실행 중단
}JavaScriptbru.runner.setNextRequest는 컬렉션 러너의 실행 흐름을 스크립트로 제어한다. 인자로 요청 이름을 넘기면 순차 실행을 무시하고 해당 요청으로 점프한다. 여기서 비교 대상은 res.status가 아니라 res.body.status다. res.status는 HTTP 상태 코드(숫자)이므로 'active' 같은 도메인 상태값과 비교하면 어떤 응답에서도 참이 되지 않는다. 실행을 멈출 때는 Postman 관용구인 setNextRequest(null) 대신 공식 API에 등재된 bru.runner.stopExecution()을 쓴다. 현재 요청만 건너뛰려면 bru.runner.skipRequest()가 따로 있다.
분기가 늘어날수록 실행 경로를 사람이 추적하기 어려워진다. 두세 갈래를 넘어가면 테스트 코드로 옮기는 쪽을 권한다.
코드 한 줄 없이 응답을 검증하는 법, Bruno Assert 탭
Bruno의 Assert 탭은 스크립트를 한 줄도 쓰지 않고 응답을 검증하는 기능이다. Expression, Operator, Value 세 칸을 채우면 그것이 곧 하나의 단언이 된다. 상태 코드 확인이나 필드 존재 여부처럼 반복적으로 쓰는 검증은 대부분 이 탭에서 끝나며, 스크립트로 옮길 필요가 없다.
res.status | equals | 200
res.body.status | equals | success
res.body.message | contains | created
res.body.id | isNotEmpty |
res.body.user.profile.email | contains | @example.com
res.body.users[0].name | equals | Alice
res.headers['content-type'] | contains | application/json
res.responseTime | lt | 1000Plaintext위 목록은 Assert 탭에 입력하는 여덟 개 단언의 예시다. Expression 칸에는 res.body.user.profile.email처럼 점 표기로 중첩 객체를 파고들 수 있고, res.body.users[0].name처럼 배열 인덱스도 그대로 쓸 수 있다. isNotEmpty처럼 값이 필요 없는 연산자는 Value 칸을 비워둔다. 마지막 줄의 res.responseTime은 응답 시간을 밀리초로 담고 있어서 성능 회귀를 잡는 최소한의 안전장치로 쓰기 좋다.
사용 가능한 연산자는 목적별로 나뉜다. 비교 연산자는 equals, notEquals, gt, gte, lt, lte가 있고, 문자열 연산자는 contains, notContains, startsWith, endsWith, matches, notMatches를 제공한다. 타입과 존재 여부를 보는 연산자로는 isNull, isNotEmpty, isEmpty, isDefined, isUndefined가 있고, 값의 성질을 확인하는 isTruthy, isFalsy, isNumber, isString, isBoolean, isArray, isJson도 준비돼 있다. 그 밖에 in, notIn, between, length가 특수 목적으로 쓰인다. 전체 목록과 각 연산자의 동작은 Assertions 공식 문서에 정리돼 있다.
한 가지 알아둘 점은 GUI에서 보이는 연산자 이름과 파일에 저장되는 이름이 다르다는 것이다. Assert 탭의 equals는 YAML에서 runtime.assertions 아래 operator: eq로 직렬화된다. 파일을 직접 편집할 일이 생기면 이 축약형을 확인해야 한다.
연산자 조합으로 표현하기 어려운 검증, 예를 들어 배열 전체를 순회하며 조건을 확인하거나 여러 필드 간 관계를 따지는 경우에는 JavaScript 테스트 스크립트로 넘어가면 된다. Bruno는 내부적으로 Chai 라이브러리를 사용하므로 expect(res.body.items).to.have.lengthOf(3) 같은 익숙한 문법이 그대로 통한다. 새 단언 문법을 익힐 일이 없다는 뜻이다.
Bruno API 클라이언트를 CI 파이프라인에 밀어 넣기
bru CLI는 컬렉션 디렉터리에서 명령 하나로 전체 요청과 테스트를 헤드리스 실행한다. 컬렉션이 이미 저장소 안에 파일로 들어 있으므로 CI가 별도로 컬렉션을 내려받을 필요가 없다는 점이 구조적 이점이다. 결과는 HTML, JSON, JUnit 세 가지 포맷으로 동시에 내보낼 수 있다.
# 컬렉션 전체 실행
bru run
# 환경 지정 실행
bru run --env local
# 단일 요청만 실행 (컬렉션 디렉터리 기준 상대 경로)
bru run users/create-user.yml
# 테스트만 실행
bru run --env local --tests-only
# 리포트 3종 동시 생성
bru run --env local \
--reporter-html results.html \
--reporter-json results.json \
--reporter-junit results.xml
# 환경 변수 오버라이드
bru run --env local --env-var baseURL=https://echo.usebruno.com
# CSV로 데이터 주도 테스트
bru run --env local users/create-user.yml \
--csv-file-path users.csv \
--reporter-html results.htmlShellScript이 명령들은 로컬 개발과 CI 양쪽에서 모두 쓰인다. --env는 앞서 만든 환경 파일명을 그대로 받고, --env-var는 환경 파일 값을 실행 시점에 덮어쓰므로 시크릿을 파일에 남기지 않고 주입하는 통로가 된다. --csv-file-path는 CSV 각 행을 변수로 치환해 같은 요청을 반복 실행하는 데이터 주도 테스트를 제공한다. 옵션 전체 목록과 동작은 bru CLI 퀵스타트 문서에 정리돼 있다.
# .github/workflows/api-test.yml
name: API Test
on: [push, pull_request]
jobs:
api-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
- name: Install Bruno CLI
run: npm install -g @usebruno/cli
- name: Run API tests
working-directory: ./api-collection
run: bru run --env ci --reporter-junit results.xml
- name: Upload report
if: always()
uses: actions/upload-artifact@v7
with:
name: bruno-report
path: ./api-collection/results.xmlYAML이 워크플로는 앞의 명령들을 GitHub Actions 형태로 조합한 예시다. 체크아웃한 저장소 안의 api-collection 디렉터리에서 bru run을 실행하고, JUnit XML을 아티팩트로 올린다. JUnit 포맷을 고른 이유는 GitHub Actions를 비롯한 대부분의 CI가 이 포맷을 테스트 리포트로 그대로 인식하기 때문이다. if: always()를 붙여야 테스트가 실패한 경우에도 리포트가 남아 원인 분석이 가능하다. 액션 버전은 2026년 7월 기준 최신 메이저인 v7 계열로 적었다.
이미 OpenAPI 스펙이 있는 프로젝트라면 컬렉션을 손으로 만들 필요가 없다. 스펙 URL이나 파일 경로를 넘기면 요청 파일이 통째로 생성된다.
bru import openapi \
--source https://petstore3.swagger.io/api/v3/openapi.json \
--output ./petstore-api \
--collection-name "Petstore API"ShellScript이 명령은 OpenAPI 스펙을 읽어 Bruno 컬렉션 디렉터리를 만들어낸다. --source는 원격 URL과 로컬 파일 경로를 모두 받고, --output은 생성될 디렉터리, --collection-name은 컬렉션 이름이 된다. 스펙이 코드에서 자동 생성되는 프로젝트라면 이 명령을 CI 단계로 넣어 컬렉션을 주기적으로 재생성할 수도 있다. 단, 기존 요청에 붙여둔 테스트 스크립트가 덮어써지지 않도록 출력 디렉터리는 반드시 분리해야 한다.
Postman에서 Bruno로 이사 올 때 짐에서 빠지는 것들

Postman에서 Bruno로 옮길 때 반드시 확인할 항목은 세 가지다. 모든 변수값이 문자열로 저장되고, 환경명은 영숫자와 -, _, . 만 허용하며 숫자로 시작할 수 없고, 여러 컬렉션을 한 번에 가져오는 Bulk Import는 유료 Ultimate Edition 기능이다. 절차 자체는 Export와 Import 두 단계로 단순하다.
절차부터 보면 이렇다. Postman에서 컬렉션을 선택하고 ··· 메뉴의 View more actions에서 Export를 눌러 Collection v2 또는 v2.1 형식의 JSON으로 내보낸 뒤, Bruno에서 Import로 그 파일을 불러오면 끝난다. 환경은 컬렉션과 별개이므로 Postman의 환경 탭에서 따로 export한 다음 Bruno에서 Import environment로 가져와야 한다. 전체 절차는 공식 마이그레이션 문서에 화면 단위로 정리돼 있다.
스크립트는 생각보다 많이 자동 변환된다. pm.test, pm.environment, pm.globals, pm.response 같은 흔히 쓰는 Postman 스크립트 API는 Bruno API 클라이언트가 임포트 과정에서 대응하는 문법으로 바꿔준다. 테스트 수십 개짜리 컬렉션도 대부분 손대지 않고 넘어오므로, 마이그레이션 비용을 과대평가할 필요는 없다.
앞서 언급한 세 항목을 조금 더 풀면 이렇다. 첫째, 모든 변수값이 문자열로 저장된다. 숫자와 불리언을 쓰는 스크립트가 있다면 임포트 직후 한 번은 훑어야 한다. 둘째, 환경명은 영숫자와 -, _, . 만 허용하며 숫자로 시작할 수 없다. Postman에서 2024-staging 같은 이름을 쓰고 있었다면 임포트 시점에 이름을 바꿔야 한다. 셋째, 여러 컬렉션을 한 번에 가져오는 Bulk Import는 유료 Ultimate Edition 기능이다. 무료 버전에서는 컬렉션을 하나씩 임포트해야 하므로 컬렉션이 수십 개인 조직은 이 부분을 사전에 고려해야 한다.
앞으로의 변화도 미리 알아둘 만하다. v4가 실험 빌드로 공개돼 있고 몇 가지 breaking change가 예고돼 있다. secrets.json 파일이 폐지되고 비밀값이 환경 파일 내부의 externalSecrets 섹션으로 이동하며, 변수에 @string, @number 같은 타입을 지정할 수 있게 된다. CLI가 생성하는 JUnit 리포트의 classname도 URL 기반에서 컬렉션 경로 기반으로 바뀐다. 아직 실험 단계이므로 운영 파이프라인에 바로 올릴 단계는 아니며, 시험해 보더라도 업그레이드 전에 컬렉션 디렉터리를 백업해 두는 것이 좋다.
Bruno, Postman, Insomnia 중 무엇을 고를까

세 도구는 저장 방식에서 갈린다. API 정의를 애플리케이션 코드와 같은 저장소에서 관리하고 요청 데이터를 외부에 두기 어려운 팀이라면 Bruno, Git을 쓰지 않는 구성원과 협업하거나 자동 문서화와 Mock 서버가 필요하면 Postman, 로컬 저장과 클라우드 동기화를 상황별로 섞고 싶다면 Insomnia가 무난하다. 아래 표는 그 판단 근거다.
※ 2026년 7월 23일 기준. Bruno v3.5.3.
| 항목 | Bruno | Postman | Insomnia |
|---|---|---|---|
| 저장 방식 | 로컬 평문 파일(YAML/BRU) | 클라우드 DB 기반 | Local Vault / Git Sync / Cloud Sync 선택 |
| Git 연동 | 컬렉션 디렉터리를 그대로 커밋 | 별도 export 후 커밋 필요 | Git Sync로 외부 저장소 직접 연동 |
| 오프라인 동작 | 완전 오프라인 | 일부 기능 제한 | Local Vault 선택 시 완전 로컬 |
| 계정 필요 여부 | 불필요 | 무료 플랜도 계정 필요 | Cloud Sync 사용 시 필요 |
| 지원 프로토콜 | REST, GraphQL, gRPC, WebSocket, SOAP | REST, GraphQL, gRPC, WebSocket | REST, GraphQL, gRPC, WebSocket, SSE |
| CLI | bru CLI 기본 제공 | Newman 별도 제공 | Inso CLI 제공 |
| 팀 협업 방식 | Git 저장소 공유 | 클라우드 워크스페이스 | Cloud Sync 또는 Git Sync |
| 자동 API 문서 생성 | 공식 기능 없음 | 클라우드 문서 생성·공개 지원 | 지원 |
| Mock 서버 | 공식 기능 없음 | 클라우드 Mock 서버 제공 | 무료 플랜 월 1,000요청 |
| 라이선스 | MIT | 상용(무료 플랜 1인 제한) | Apache-2.0 |
| 유료 플랜(사용자당 월) | Pro $6, Ultimate $11 | Team $19 | Pro $12, Enterprise $45 |
| GitHub 스타 | 45.8k | 비공개 | 39.9k |
표에서 눈여겨볼 지점은 프로토콜과 가격 행이다. Bruno API 클라이언트를 REST 전용 도구로 오해하기 쉬운데, WebSocket은 2.13.0부터 들어왔고 gRPC는 베타를 거쳐 정식 기능이 됐다. SOAP 요청도 지원한다. 무료 버전에서 REST, GraphQL, gRPC를 포함한 기본 API 클라이언트 기능과 테스트, 스크립팅을 모두 쓸 수 있고 워크스페이스는 2개까지 허용된다. 그 이상이 필요하면 Pro가 사용자당 월 6달러다. Kong이 관리하는 Insomnia는 Apache-2.0 라이선스에 SSE까지 다루고 Local Vault, Git Sync, Cloud Sync를 조합해 쓸 수 있어서, 저장 위치는 통제하고 싶은데 클라우드 협업도 포기하기 어려운 경우의 절충안이 된다. 다만 무료 플랜의 Git Sync는 3명까지이고 Pro는 사용자당 월 12달러로 Bruno보다 높다.
반대로 기획자나 QA, 외부 파트너처럼 Git을 쓰지 않는 구성원이 컬렉션을 함께 다뤄야 한다면 Postman의 워크스페이스 모델이 여전히 유리하다. API 문서를 자동으로 생성해 외부에 공개하거나 Mock 서버로 프런트엔드 개발을 병행해야 하는 경우도 마찬가지다. 2026년 3월 무료 플랜이 1인으로 제한되면서 팀 단위 사용은 유료 전환이 전제가 됐다는 점만 계산에 넣으면 된다.
셋 중 하나만 골라야 하는 것도 아니다. 개발자 개인 작업과 CI 자동화는 Bruno API 클라이언트로 처리하고 외부 공개용 문서와 Mock 서버는 기존 도구를 유지하는 조합이 실제로 흔하다. 컬렉션이 파일이라는 특성 덕분에 병행 운영 비용도 낮은 편이다.
마치며

지금까지 Bruno API 클라이언트에 대해서 정리해 보았다.
이 도구에 관심을 갖게 된 계기는 사소한 사고였다. 팀 워크스페이스에서 공유하던 Postman 컬렉션의 어떤 요청 바디에서 필수 필드 하나가 조용히 사라져 있었고, 누가 언제 왜 그렇게 바꿨는지 아무도 답하지 못했다. 워크스페이스에는 히스토리가 남지 않았고, 결국 서버 로그를 뒤져서 그 필드가 언제부터 안 들어왔는지 역산해야 했다. 반나절이 날아갔다. 도구가 고장 난 게 아니었다. 요청 정의에만 버전 관리가 없었을 뿐이다.
컬렉션을 저장소 안으로 옮기고 나서 가장 편해진 지점은 리뷰였다. 엔드포인트에 파라미터를 추가하면 컨트롤러 코드 변경과 요청 파일 변경이 같은 PR에 묶여서 올라온다. 리뷰어는 코드만 보고 명세를 추측하는 대신 실제 요청 예시를 나란히 확인한다. 사소해 보이지만 API 스펙 논의가 코멘트 몇 줄로 끝나는 경우가 눈에 띄게 늘었다.
물론 모든 상황에서 답이 되지는 않는다. 외부 파트너에게 API 문서를 열어주거나 프런트엔드 개발용 Mock 서버를 붙여야 하는 조직에서는 Bruno API 클라이언트만으로 기존 도구를 완전히 대체하기 어렵다. 그런 팀이라면 개발자 일상 작업과 CI 검증만 옮기고 문서화는 그대로 두는 절충이 현실적이다. 도구를 통째로 갈아엎는 것보다 컬렉션 하나를 시험 삼아 저장소에 넣어보고 첫 PR에서 diff가 어떻게 보이는지 확인하는 편이, 이 방식이 팀에 맞는지 판단하는 가장 빠른 길이다.
FAQ
Bruno는 무료인가?
Bruno의 데스크톱 앱과 bru CLI는 MIT 라이선스 오픈소스로 무료다. 계정 생성이나 결제 없이 요청 작성, 환경 관리, 테스트 실행, 스크립팅, CI 연동을 쓸 수 있고 REST, GraphQL, gRPC도 무료 범위에 들어간다. 다만 무료 버전은 워크스페이스가 2개로 제한되며, 여러 컬렉션을 한 번에 가져오는 Bulk Import는 유료 Ultimate Edition 기능이다. 유료 플랜은 Pro가 사용자당 월 6달러, Ultimate가 11달러다(연간 결제 기준).
Bruno 컬렉션을 Git에 올릴 때 API 키는 어떻게 관리하나?
호스트나 API 버전처럼 팀 공용인 값만 담은 환경 파일을 커밋하고, 토큰과 시크릿은 별도 파일로 분리해 .gitignore에 등록하는 방식이 일반적이다. CI에서는 bru run --env-var KEY=VALUE 형태로 실행 시점에 값을 주입하면 비밀값이 저장소에 남지 않는다. 예고된 v4에서는 환경 파일 내부의 externalSecrets 섹션으로 비밀값 관리 방식이 바뀔 예정이다.
Postman 스크립트를 그대로 가져다 쓸 수 있나?
pm.test, pm.environment 같은 흔히 쓰이는 Postman 스크립트 API는 임포트 과정에서 Bruno 문법으로 자동 변환된다. 대부분의 테스트 코드가 수정 없이 동작하지만, 임포트 시 모든 변수값이 문자열로 저장되므로 숫자나 불리언을 비교하는 스크립트는 한 번 확인해야 한다. 환경명이 숫자로 시작하거나 허용되지 않은 문자를 포함하면 이름도 바꿔야 한다.
기존 .bru 파일을 YAML로 꼭 바꿔야 하나?
바꾸지 않아도 된다. .bru 포맷은 계속 지원되며 한 컬렉션 안에서 YAML과 공존할 수 있다. 다만 공식 벤치마크상 YAML 파싱이 BRU 대비 4~5배 빠르고 대형 컬렉션에서 확장성이 좋으므로, 요청 수가 많은 컬렉션이라면 전환을 검토할 만하다. 마이그레이션은 export 후 re-import 방식으로 무손실 진행된다.
Bruno가 Postman을 완전히 대체할 수 있나?
개발자 개인 작업과 CI 자동화는 대체할 수 있다. 요청 작성, 환경 관리, 테스트, CLI 실행이 무료로 제공되고 REST와 GraphQL은 물론 gRPC, WebSocket, SOAP까지 다룬다. 여기에 컬렉션이 Git으로 관리된다는 이점이 더해진다. 다만 외부 공개용 자동 API 문서화와 Mock 서버는 Bruno에 공식 기능이 없으므로, 그 기능이 필요한 팀은 두 도구를 병행하는 조합이 현실적이다.
