JavaScript Base64 디코딩 완전 가이드 — atob() vs Buffer

·Front-end & Node.js Developer·검토자Sophie Laurent·게시일

무료 Base64 디코더을 브라우저에서 직접 사용하세요 — 설치 불필요.

Base64 디코더 온라인으로 사용하기 →

프로덕션 인증 이슈를 디버깅할 때 제가 가장 먼저 찾는 것은 Base64 디코더입니다 — JWT 페이로드, 웹훅 서명, 인코딩된 설정 값이 모두 Base64 문자열 안에 숨어 있습니다. JavaScript는 Base64 디코딩을 위한 두 가지 주요 내장 방식을 제공합니다: atob()(브라우저 + Node.js 16+)와 Buffer.from(encoded, 'base64').toString() (Node.js) — 원본 데이터에 유니코드 문자가 포함된 경우 두 방식의 동작이 크게 다릅니다. 코드 작성 없이 빠르게 디코딩하려면, ToolDeck's Base64 Decoder 가 브라우저에서 즉시 처리해 드립니다. 이 가이드는 두 환경 모두를 다룹니다 — Node.js 16+와 최신 브라우저(Chrome 80+, Firefox 75+, Safari 14+)를 대상으로 — 프로덕션에서 바로 쓸 수 있는 예제를 담고 있습니다: UTF-8 복원, URL 안전 변형, JWT 디코딩, 파일, API 응답, Node.js 스트림, 그리고 실제 코드베이스에서 반복적으로 깨진 출력을 만들어내는 네 가지 실수들입니다.

  • atob(encoded)는 브라우저 네이티브이며 Node.js 16+에서도 전역으로 사용 가능하지만, 바이너리 문자열을 반환합니다 — ASCII를 초과하는 모든 콘텐츠에서 UTF-8 텍스트를 복원하려면 TextDecoder를 사용하세요.
  • Buffer.from(encoded, "base64").toString("utf8")은 Node.js의 관용적인 접근 방식으로 추가 단계 없이 UTF-8을 자동으로 처리합니다.
  • URL 안전 Base64(JWT에서 사용)는 +를 -로, /를 _로 바꾸고 = 패딩을 제거합니다. atob()를 호출하기 전에 이를 복원하거나 Node.js 18+에서 Buffer.from(encoded, "base64url").toString()을 사용하세요.
  • 디코딩 전에 공백과 개행을 제거하세요 — GitHub Contents API와 많은 MIME 인코더는 Base64 출력을 줄당 60~76자로 줄 바꿈합니다.
  • Uint8Array.prototype.fromBase64()(TC39 Stage 3)는 이미 Node.js 22+와 Chrome 130+에서 사용 가능하며, 결국 두 환경을 통합할 것입니다.

Base64 디코딩이란?

Base64 디코딩은 인코딩의 역과정입니다 — 64개 문자 ASCII 표현을 원래의 바이너리 데이터나 텍스트로 되돌립니다. 4개의 Base64 문자는 정확히 3바이트로 다시 매핑됩니다. 인코딩된 문자열 끝의 = 패딩 문자는 마지막 3바이트 그룹을 완성하기 위해 몇 바이트가 추가되었는지를 디코더에 알려줍니다.

Base64는 암호화가 아닙니다 — 인코딩된 문자열을 가진 누구든지 완전히 역산할 수 있습니다. 그 목적은 전송 안전성입니다: 7비트 ASCII 텍스트용으로 설계된 프로토콜과 저장 형식은 임의의 바이너리 바이트를 처리할 수 없으며, Base64가 그 간극을 메웁니다. JavaScript에서 흔한 디코딩 시나리오로는 JWT 페이로드 검사, 환경 변수에서 Base64 인코딩된 JSON 설정 해제, REST API에서 바이너리 파일 콘텐츠 추출, 브라우저에서 data URI 디코딩 등이 있습니다.

Before · text
After · text
ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=
deploy-bot:sk-prod-a7f2c91e4b3d8

atob() — 브라우저 네이티브 디코딩 함수

atob()(ASCII-to-binary)는 IE10부터 브라우저에서 사용 가능했으며 WinterCG 호환성 이니셔티브의 일환으로 Node.js 16.0에서 전역 함수가 되었습니다. Deno, Bun, Cloudflare Workers에서도 네이티브로 동작하며 import가 필요 없습니다.

이 함수는 바이너리 문자열을 반환합니다: 각 문자의 코드 포인트가 하나의 원시 바이트 값(0–255)과 같은 JavaScript 문자열입니다. 이것이 중요한 이유는: 원본 데이터가 U+007F 이상의 문자 (악센트 문자, 키릴 문자, CJK, 이모지)를 포함한 UTF-8 텍스트였다면, 반환된 문자열은 읽을 수 있는 텍스트가 아닌 원시 바이트 시퀀스입니다. 복원하려면 TextDecoder를 사용하세요(다음 섹션에서 다룹니다).

최소 동작 예제

JavaScript (browser / Node.js 16+)
// Decoding an HTTP Basic Auth credential pair received in a request header
// Authorization: Basic ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=

function parseBasicAuth(header: string): { serviceId: string; apiKey: string } {
  const base64Part = header.replace(/^Basics+/i, '')
  const decoded    = atob(base64Part)
  const [serviceId, apiKey] = decoded.split(':')
  return { serviceId, apiKey }
}

const auth = parseBasicAuth('Basic ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=')

console.log(auth.serviceId) // deploy-bot
console.log(auth.apiKey)    // sk-prod-a7f2c91e4b3d8

왕복 검증

JavaScript
// Verify lossless recovery for ASCII-only content
const original = 'service:payments region:eu-west-1 env:production'

const encoded = btoa(original)
const decoded = atob(encoded)

console.log(encoded)
// c2VydmljZTpwYXltZW50cyByZWdpb246ZXUtd2VzdC0xIGVudjpwcm9kdWN0aW9u

console.log(decoded === original) // true
참고:atob()btoa() WinterCG Minimum Common API 의 일부입니다 — 비브라우저 런타임에서 Fetch, URL, crypto를 관장하는 동일한 사양입니다. Node.js 16+, Bun, Deno, Cloudflare Workers에서 동일하게 동작합니다.

디코딩 후 UTF-8 텍스트 복원

atob()의 가장 흔한 함정은 반환 타입을 잘못 이해하는 것입니다. 원본 텍스트가 Base64로 인코딩되기 전에 UTF-8로 인코딩되었다면,atob()는 읽을 수 있는 텍스트가 아닌 Latin-1 바이너리 문자열을 반환합니다:

JavaScript
// '김민준' was UTF-8 encoded then Base64 encoded before transmission
const encoded = '6rmA66mU7KSA'

// ❌ atob() returns the raw UTF-8 bytes as a Latin-1 string — garbled output
console.log(atob(encoded))
// "김민ì€"  ← byte values misread as Latin-1

올바른 접근 방식은 TextDecoder를 사용하여 원시 바이트를 UTF-8로 해석하는 것입니다:

TextDecoder 접근 방식 — 모든 유니코드 출력에 안전

JavaScript (browser + Node.js 16+)
// Unicode-safe Base64 decode utilities
function fromBase64(encoded: string): string {
  const binary = atob(encoded)
  const bytes  = Uint8Array.from(binary, ch => ch.charCodeAt(0))
  return new TextDecoder().decode(bytes)
}

function toBase64(text: string): string {
  const bytes = new TextEncoder().encode(text)
  const chars = Array.from(bytes, byte => String.fromCharCode(byte))
  return btoa(chars.join(''))
}

// Works with any language or script
const orderNote = '확인됨: 김민준 — 서울 창고, 수량: 250'
const encoded   = toBase64(orderNote)
const decoded   = fromBase64(encoded)

console.log(decoded === orderNote) // true
console.log(decoded)
// 확인됨: 김민준 — 서울 창고, 수량: 250
참고:Node.js에서는 TextDecoder 단계를 완전히 건너뛸 수 있습니다 — Buffer.from(encoded, 'base64').toString('utf8')를 사용하세요. 디코딩된 바이트를 자동으로 UTF-8로 해석하며 대용량 입력에서 더 빠릅니다.

Node.js의 Buffer.from() — 완전한 디코딩 가이드

Node.js에서 Buffer는 Base64 디코딩을 포함한 모든 바이너리 작업을 위한 관용적인 API입니다. UTF-8을 네이티브로 처리하고, 진정한 Buffer(바이너리 안전)를 반환하며, Node.js 18부터 URL 안전 변형을 위한 'base64url' 인코딩 단축키를 지원합니다.

환경 변수 설정 디코딩

Node.js
// Server config stored as Base64 in an env variable (avoids JSON escaping in shell)
// DB_CONFIG=eyJob3N0IjoiZGItcHJpbWFyeS5pbnRlcm5hbCIsInBvcnQiOjU0MzIsImRhdGFiYXNlIjoiYW5hbHl0aWNzX3Byb2QiLCJtYXhDb25uZWN0aW9ucyI6MTAwfQ==

const raw = Buffer.from(process.env.DB_CONFIG!, 'base64').toString('utf8')
const dbConfig = JSON.parse(raw)

console.log(dbConfig.host)           // db-primary.internal
console.log(dbConfig.port)           // 5432
console.log(dbConfig.maxConnections) // 100

.b64 파일에서 바이너리 파일 복원

Node.js
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'

// Read the Base64-encoded certificate and restore the original binary
const encoded = readFileSync(join(process.cwd(), 'dist', 'cert.b64'), 'utf8').trim()
const certBuf  = Buffer.from(encoded, 'base64')

writeFileSync('./ssl/server.crt', certBuf)

console.log(`Restored ${certBuf.length} bytes`)
// Restored 2142 bytes

오류 처리가 포함된 비동기 디코딩

Node.js
import { readFile, writeFile } from 'node:fs/promises'

async function decodeBase64File(
  encodedPath: string,
  outputPath:  string,
): Promise<number> {
  try {
    const encoded = await readFile(encodedPath, 'utf8')
    const binary  = Buffer.from(encoded.trim(), 'base64')
    await writeFile(outputPath, binary)
    return binary.length
  } catch (err) {
    const code = (err as NodeJS.ErrnoException).code
    if (code === 'ENOENT') throw new Error(`File not found: ${encodedPath}`)
    if (code === 'EACCES') throw new Error(`Permission denied: ${encodedPath}`)
    throw err
  }
}

// Restore a PDF stored as Base64
const bytes = await decodeBase64File('./uploads/invoice.b64', './out/invoice.pdf')
console.log(`Decoded ${bytes} bytes — PDF restored`)

Base64 디코딩 함수 — 파라미터 레퍼런스

코드를 작성하거나 리뷰할 때 참조할 수 있는 두 가지 주요 네이티브 디코딩 API 파라미터 빠른 레퍼런스입니다.

atob(encodedData)

파라미터타입필수설명
encodedDatastring+, /, = 문자를 사용하는 표준 Base64 문자열. URL 안전 변형(-, _)은 InvalidCharacterError를 던집니다. 공백은 허용되지 않습니다.
반환값: 바이너리 문자열 — 각 문자의 코드 포인트가 하나의 원시 바이트 값(0–255)과 같습니다. 유니코드 문자열이 아니므로, UTF-8 텍스트를 복원하려면 TextDecoder를 통과시켜야 합니다.

Buffer.from(input, inputEncoding) / .toString(outputEncoding)

파라미터타입기본값설명
inputstring | Buffer | TypedArray | ArrayBuffer필수디코딩할 Base64 인코딩된 문자열, 또는 인코딩된 바이트를 포함하는 Buffer.
inputEncodingBufferEncoding"utf8"표준 Base64(RFC 4648 §4)에는 "base64", URL 안전 Base64(RFC 4648 §5, Node.js 18+)에는 "base64url"로 설정.
outputEncodingstring"utf8".toString() 출력의 인코딩. 읽을 수 있는 텍스트에는 "utf8", atob() 출력과 호환되는 Latin-1 바이너리 문자열에는 "binary" 사용.
startinteger0읽기 시작할 디코딩된 Buffer 내의 바이트 오프셋. .toString()의 두 번째 인수로 전달됨.
endintegerbuf.length읽기를 중지할 바이트 오프셋(제외). .toString()의 세 번째 인수로 전달됨.
반환값: .from()에서 Buffer를 반환합니다. .toString()에서 문자열을 반환합니다. 디코딩된 콘텐츠가 바이너리(이미지, PDF, 오디오)인 경우 Buffer로 유지하세요(.toString()을 호출하지 말 것) — writeFile이나 응답 스트림에 직접 전달하세요.

URL 안전 Base64 — JWT와 URL 파라미터 디코딩

JWT는 세 개의 세그먼트 모두에 URL 안전 Base64(RFC 4648 §5)를 사용합니다. URL 안전 Base64는 + -로, / _로 바꾸고, 말미의 = 패딩을 제거합니다. 복원 없이 이것을 atob()에 전달하면 잘못된 출력이 나오거나 예외가 발생합니다.

브라우저 — 디코딩 전 문자와 패딩 복원

JavaScript (browser)
function decodeBase64Url(input: string): string {
  const base64 = input.replace(/-/g, '+').replace(/_/g, '/')
  const padded = base64 + '==='.slice(0, (4 - base64.length % 4) % 4)
  const binary = atob(padded)
  const bytes  = Uint8Array.from(binary, ch => ch.charCodeAt(0))
  return new TextDecoder().decode(bytes)
}

// Inspect a JWT payload segment (the middle part between the two dots)
const jwtToken  = 'eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2U4YjRkIiwicm9sZSI6ImVkaXRvciIsIndvcmtzcGFjZUlkIjoid3NfM2E3ZjkxYzIiLCJleHAiOjE3MTcyMDM2MDB9'
const payload   = decodeBase64Url(jwtToken)
const claims    = JSON.parse(payload)

console.log(claims.userId)      // usr_9f2a1c3e8b4d
console.log(claims.role)        // editor
console.log(claims.workspaceId) // ws_3a7f91c2

Node.js 18+ — 네이티브 'base64url' 인코딩

Node.js 18+
// Node.js 18 added 'base64url' as a first-class Buffer encoding — no manual replace needed
function decodeJwtSegment(segment: string): Record<string, unknown> {
  const json = Buffer.from(segment, 'base64url').toString('utf8')
  return JSON.parse(json)
}

const token   = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2U4YjRkIiwicm9sZSI6ImVkaXRvciIsIndvcmtzcGFjZUlkIjoid3NfM2E3ZjkxYzIiLCJleHAiOjE3MTcyMDM2MDB9.SIGNATURE'
const [headerB64, payloadB64] = token.split('.')

const header  = decodeJwtSegment(headerB64)
const payload = decodeJwtSegment(payloadB64)

console.log(header.alg)          // HS256
console.log(payload.role)        // editor
console.log(payload.workspaceId) // ws_3a7f91c2

파일과 API 응답에서 Base64 디코딩

프로덕션 코드에서 Base64 디코딩은 인코딩된 형태로 콘텐츠를 전달하는 외부 API를 소비할 때 가장 자주 발생합니다. 두 시나리오 모두 공백 처리와 바이너리 vs 텍스트 출력에 관한 중요한 주의 사항이 있습니다. 디버깅 중에 인코딩된 응답을 확인하기만 하려면, 직접 Base64 Decoder 에 붙여넣으세요 — 표준과 URL 안전 모드를 즉시 처리합니다.

GitHub Contents API에서 콘텐츠 디코딩

JavaScript
// GitHub Contents API returns file content as Base64, wrapped at 60 chars per line
async function fetchDecodedFile(
  owner: string,
  repo:  string,
  path:  string,
  token: string,
): Promise<string> {
  const res = await fetch(
    `https://api.github.com/repos/${owner}/${repo}/contents/${path}`,
    { headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json' } }
  )
  if (!res.ok) throw new Error(`GitHub API ${res.status}: ${res.statusText}`)

  const data = await res.json() as { content: string; encoding: string }
  if (data.encoding !== 'base64') throw new Error(`Unexpected encoding: ${data.encoding}`)

  // ⚠️ GitHub wraps at 60 chars — strip newlines before decoding
  const clean = data.content.replace(/\n/g, '')
  return Buffer.from(clean, 'base64').toString('utf8')
}

const openApiSpec = await fetchDecodedFile('acme-corp', 'platform-api', 'openapi.json', process.env.GITHUB_TOKEN!)
const spec = JSON.parse(openApiSpec)
console.log(`API version: ${spec.info.version}`)

API에서 Base64 인코딩된 바이너리 디코딩(브라우저)

JavaScript (browser)
// Some APIs return binary content (images, PDFs) as Base64 JSON fields
async function downloadDecodedFile(endpoint: string, authToken: string): Promise<void> {
  const res = await fetch(endpoint, { headers: { Authorization: `Bearer ${authToken}` } })
  if (!res.ok) throw new Error(`Download failed: ${res.status}`)

  const { filename, content, mimeType } = await res.json() as {
    filename: string; content: string; mimeType: string
  }

  // Decode Base64 → binary bytes → Blob
  const binary = atob(content)
  const bytes  = Uint8Array.from(binary, ch => ch.charCodeAt(0))
  const blob   = new Blob([bytes], { type: mimeType })

  // Trigger browser download
  const url = URL.createObjectURL(blob)
  const a   = Object.assign(document.createElement('a'), { href: url, download: filename })
  a.click()
  URL.revokeObjectURL(url)
}

await downloadDecodedFile('/api/reports/latest', sessionStorage.getItem('auth_token')!)

Node.js와 셸에서 커맨드라인 Base64 디코딩

CI/CD 스크립트, 디버깅 세션, 또는 일회성 디코딩 작업에는 셸 도구와 Node.js 원라이너가 완전한 스크립트보다 빠릅니다. macOS와 Linux에서 플래그 이름이 다르다는 점을 주의하세요.

bash
# ── macOS / Linux system base64 ───────────────────────────────────────
# Standard decoding (macOS uses -D, Linux uses -d)
echo "ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=" | base64 -d   # Linux
echo "ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=" | base64 -D   # macOS

# Decode a .b64 file to its original binary
base64 -d ./dist/cert.b64 > ./ssl/server.crt       # Linux
base64 -D -i ./dist/cert.b64 -o ./ssl/server.crt   # macOS

# URL-safe Base64 — restore + and / before decoding
echo "eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2UifQ" | tr '-_' '+/' | base64 -d

# ── Node.js one-liner — works on Windows too ───────────────────────────
node -e "process.stdout.write(Buffer.from(process.argv[1], 'base64').toString())" "ZGVwbG95LWJvdA=="
# deploy-bot

# URL-safe (Node.js 18+)
node -e "process.stdout.write(Buffer.from(process.argv[1], 'base64url').toString())" "eyJhbGciOiJIUzI1NiJ9"
# {"alg":"HS256"}
참고:macOS에서 base64는 디코딩에 -D(대문자)를 사용하지만, Linux는 -d(소문자)를 사용합니다. 이로 인해 CI 스크립트가 조용히 실패합니다 — 대상 플랫폼이 Linux임이 보장되지 않는 경우 Node.js 원라이너를 사용하세요.

고성능 대안: js-base64

라이브러리를 선택하는 주된 이유는 크로스 환경 일관성입니다. 번들러 설정 없이 브라우저와 Node.js 모두에서 실행되는 패키지를 배포한다면,Buffer는 환경 감지가 필요하고atob()는 TextDecoder 우회 방법이 필요합니다.js-base64(npm 주간 다운로드 1억 이상)는 두 경우를 투명하게 처리합니다.

bash
npm install js-base64
# or
pnpm add js-base64
JavaScript
import { fromBase64, fromBase64Url, isValid } from 'js-base64'

// Standard decoding — Unicode-safe, works in browser and Node.js
const raw   = fromBase64('eyJldmVudElkIjoiZXZ0XzdjM2E5ZjFiMmQiLCJ0eXBlIjoiY2hlY2tvdXRfY29tcGxldGVkIiwiY3VycmVuY3kiOiJFVVIiLCJhbW91bnQiOjE0OTAwfQ==')
const event = JSON.parse(raw)
console.log(event.type)     // checkout_completed
console.log(event.currency) // EUR

// URL-safe decoding — no manual character replacement needed
const jwtPayload = fromBase64Url('eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2U4YjRkIiwicm9sZSI6ImVkaXRvciJ9')
const claims     = JSON.parse(jwtPayload)
console.log(claims.role) // editor

// Validate before decoding untrusted input
const untrusted = 'not!valid@base64#'
if (!isValid(untrusted)) {
  console.error('Rejected: invalid Base64 input')
}

// Binary output — second argument true returns Uint8Array
const pngBytes = fromBase64('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', true)
console.log(pngBytes instanceof Uint8Array) // true

구문 강조가 포함된 터미널 출력

CLI 디버깅 도구나 검사 스크립트를 작성할 때, 대용량 JSON 페이로드에 대해 일반 console.log 출력은 읽기 어렵습니다.chalk(터미널 색상화를 위한 가장 많이 다운로드된 npm 패키지)와 Base64 디코딩을 결합하면 읽기 쉽고 스캔하기 좋은 터미널 출력을 만들 수 있습니다 — JWT 검사, API 응답 디버깅, 설정 감사에 유용합니다.

bash
npm install chalk
# chalk v5+ is ESM-only — use import, not require
Node.js
import chalk from 'chalk'

// Decode and display any Base64 value with smart type detection
function inspectBase64(encoded: string, label = 'Decoded value'): void {
  let decoded: string
  try {
    decoded = Buffer.from(encoded.trim(), 'base64').toString('utf8')
  } catch {
    console.error(chalk.red('✗ Invalid Base64 input'))
    return
  }

  console.log(chalk.bold.cyan(`\n── ${label} ──`))

  // Attempt JSON pretty-print
  try {
    const parsed = JSON.parse(decoded)
    console.log(chalk.green('Type:'), chalk.yellow('JSON'))
    for (const [key, value] of Object.entries(parsed)) {
      const display = typeof value === 'object' ? JSON.stringify(value) : String(value)
      console.log(chalk.green(`  ${key}:`), chalk.white(display))
    }
    return
  } catch { /* not JSON */ }

  // Plain text fallback
  console.log(chalk.green('Type:'), chalk.yellow('text'))
  console.log(chalk.white(decoded))
}

// Inspect a Base64-encoded JWT payload
const tokenParts = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2U4YjRkIiwicm9sZSI6ImVkaXRvciIsImV4cCI6MTcxNzIwMzYwMH0.SIGNATURE'.split('.')
inspectBase64(tokenParts[0], 'JWT Header')
inspectBase64(tokenParts[1], 'JWT Payload')
// ── JWT Header ──
// Type:   JSON
//   alg:  HS256
//   typ:  JWT
//
// ── JWT Payload ──
// Type:   JSON
//   userId: usr_9f2a1c3e8b4d
//   role:   editor
//   exp:    1717203600
참고:chalk는 터미널/CLI 출력에만 사용하세요 — 파일, API 응답, 로그 애그리게이터에 쓰는 콘텐츠에는 절대 사용하지 마세요. ANSI 이스케이프 코드는 비터미널 소비자를 손상시킵니다: 로그 플랫폼(Datadog, Splunk), JSON 로그 파서, CI 로그 뷰어는 모두 읽을 수 없는 문자 시퀀스로 표시합니다.

Node.js 스트림을 활용한 대용량 Base64 파일 디코딩

Base64 인코딩된 파일이 약 50 MB를 초과하면, readFileSync()로 전체를 메모리에 로드하는 것이 문제가 됩니다. Node.js 스트림을 사용하면 청크 단위로 데이터를 디코딩할 수 있습니다 — 하지만 Base64는 청크 경계에서 패딩 오류를 피하기 위해 청크당 4개 문자의 배수가 필요합니다(각 4문자 그룹이 정확히 3바이트로 디코딩됩니다).

Node.js
import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'

async function streamDecodeBase64(inputPath: string, outputPath: string): Promise<void> {
  const readStream  = createReadStream(inputPath, { encoding: 'utf8', highWaterMark: 4 * 1024 * 192 })
  const writeStream = createWriteStream(outputPath)

  let buffer = ''

  await pipeline(
    readStream,
    async function* (source) {
      for await (const chunk of source) {
        buffer += (chunk as string).replace(/\s/g, '') // strip any whitespace/newlines

        // Decode only complete 4-char groups to avoid mid-stream padding issues
        const remainder = buffer.length % 4
        const safe      = buffer.slice(0, buffer.length - remainder)
        buffer          = buffer.slice(buffer.length - remainder)

        if (safe.length > 0) yield Buffer.from(safe, 'base64')
      }
      if (buffer.length > 0) yield Buffer.from(buffer, 'base64')
    },
    writeStream,
  )
}

// Decode a 200 MB video that was stored as Base64
await streamDecodeBase64('./uploads/product-demo.b64', './dist/product-demo.mp4')
console.log('Stream decode complete')
참고:Base64 텍스트를 읽을 때 청크 크기는 4문자의 배수여야 합니다. 각 청크에 완전한 4문자 그룹만 포함되도록 합니다. 예제에서는 4 × 1024 × 192 = 786,432자(768 KB)를 사용합니다. 50 MB 미만의 파일에는 readFile() + Buffer.from(content.trim(), 'base64')가 더 단순하고 충분히 빠릅니다.

흔한 실수들

저는 이 네 가지 실수를 JavaScript 코드베이스에서 반복적으로 봤습니다 — 비 ASCII 문자나 줄 바꿈된 API 응답이 프로덕션의 디코딩 경로에 도달할 때까지 숨어 있는 경향이 있습니다.

실수 1 — UTF-8 콘텐츠에 TextDecoder 없이 atob() 사용

문제: atob()는 각 문자가 하나의 원시 바이트 값을 나타내는 바이너리 문자열을 반환합니다. UTF-8 멀티바이트 시퀀스(키릴 문자, CJK, 악센트 문자)가 깨진 Latin-1 문자로 나타납니다. 수정: 출력을 TextDecoder로 감싸세요.

Before · JavaScript
After · JavaScript
// ❌ atob() returns the raw UTF-8 bytes as a Latin-1 string
const encoded = '0JDQu9C10LrRgdC10Lkg0JjQstCw0L3QvtCy'
const decoded  = atob(encoded)
console.log(decoded)
// "Алексей Р˜РІР°РЅРѕРІ"  ← wrong
// ✅ Use TextDecoder to correctly interpret the UTF-8 bytes
const encoded  = '0JDQu9C10LrRgdC10Lkg0JjQstCw0L3QvtCy'
const binary   = atob(encoded)
const bytes    = Uint8Array.from(binary, ch => ch.charCodeAt(0))
const decoded  = new TextDecoder().decode(bytes)
console.log(decoded) // Алексей Иванов ✓

실수 2 — URL 안전 Base64를 직접 atob()에 전달

문제: JWT 세그먼트는 + / 대신 - _를 사용하며 패딩이 없습니다.atob()는 잘못된 결과를 반환하거나 예외를 던질 수 있습니다. 수정: 먼저 표준 문자를 복원하고 패딩을 추가하세요.

Before · JavaScript
After · JavaScript
// ❌ URL-safe JWT segment passed directly — unreliable
const jwtPayload = 'eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2UifQ'
const decoded    = atob(jwtPayload) // May produce wrong result or throw
// ✅ Restore standard Base64 chars and padding first
function decodeBase64Url(input: string): string {
  const b64  = input.replace(/-/g, '+').replace(/_/g, '/')
  const pad  = b64 + '==='.slice(0, (4 - b64.length % 4) % 4)
  const bin  = atob(pad)
  const bytes = Uint8Array.from(bin, ch => ch.charCodeAt(0))
  return new TextDecoder().decode(bytes)
}
const decoded = decodeBase64Url('eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2UifQ')
// {"userId":"usr_9f2a1c3e"} ✓

실수 3 — 줄 바꿈된 Base64에서 개행 제거 안 함

문제: GitHub Contents API와 MIME 인코더는 Base64 출력을 줄당 60~76자로 줄 바꿈합니다.atob() \n 문자에서 InvalidCharacterError를 던집니다. 수정: 디코딩 전에 모든 공백을 제거하세요.

Before · JavaScript
After · JavaScript
// ❌ GitHub API content field contains embedded newlines
const data    = await res.json()
const decoded = atob(data.content) // ❌ throws InvalidCharacterError
// ✅ Strip newlines (and any other whitespace) before decoding
const data    = await res.json()
const clean   = data.content.replace(/\s/g, '')
const decoded = atob(clean) // ✓

실수 4 — 디코딩된 바이너리 콘텐츠에 .toString() 호출

문제: 원본 데이터가 바이너리(이미지, PDF, 오디오)인 경우, .toString('utf8')를 호출하면 인식할 수 없는 바이트 시퀀스를 U+FFFD로 교체하여 출력을 조용히 손상시킵니다. 수정: 결과를 Buffer로 유지하세요 — 문자열로 변환하지 마세요.

Before · JavaScript
After · JavaScript
// ❌ .toString('utf8') corrupts binary content
import { readFileSync, writeFileSync } from 'node:fs'
const encoded   = readFileSync('./uploads/invoice.b64', 'utf8').trim()
const corrupted = Buffer.from(encoded, 'base64').toString('utf8') // ❌
writeFileSync('./out/invoice.pdf', corrupted) // ❌ unreadable PDF
// ✅ Keep the Buffer as binary — do not convert to a string
import { readFileSync, writeFileSync } from 'node:fs'
const encoded = readFileSync('./uploads/invoice.b64', 'utf8').trim()
const binary  = Buffer.from(encoded, 'base64') // ✓ raw bytes preserved
writeFileSync('./out/invoice.pdf', binary)      // ✓ valid PDF

JavaScript Base64 디코딩 방법 — 빠른 비교

방법UTF-8 출력바이너리 출력URL 안전환경설치 필요
atob()❌ TextDecoder 필요✅ 바이너리 문자열❌ 수동 복원Browser, Node 16+, Bun, Deno아니요
TextDecoder + atob()✅ UTF-8✅ Uint8Array 경유❌ 수동 복원Browser, Node 16+, Deno아니요
Buffer.from().toString()✅ utf8✅ Buffer로 유지✅ base64url (Node 18+)Node.js, Bun아니요
Uint8Array.fromBase64() (TC39)✅ TextDecoder 경유✅ 네이티브✅ alphabet 옵션Chrome 130+, Node 22+아니요
js-base64✅ 항상✅ Uint8Array✅ 내장범용npm install

디코딩된 콘텐츠가 ASCII 텍스트임이 보장될 때만 atob()를 선택하세요. 브라우저에서 사용자 입력이나 다국어 텍스트에는 TextDecoder + atob()를 사용하세요. Node.js 서버 사이드 코드에는 Buffer가 올바른 기본값입니다 — UTF-8을 자동으로 처리하고 바이너리 데이터를 그대로 유지합니다. 크로스 환경 라이브러리에는 js-base64가 모든 엣지 케이스를 제거합니다.

자주 묻는 질문

atob()가 읽을 수 있는 텍스트 대신 깨진 문자를 반환하는 이유는 무엇인가요?
atob()는 각 문자가 하나의 원시 바이트(0–255)를 나타내는 바이너리 문자열을 반환합니다. 유니코드 코드 포인트가 아닙니다. 원본 텍스트가 UTF-8로 인코딩되었다면, U+007F 이상의 모든 문자——키릴 문자, 아랍 문자, CJK 표의 문자, 악센트 문자——는 두 개 이상의 깨진 Latin-1 문자로 나타납니다. 수정 방법: 출력을 TextDecoder에 통과시킵니다: const bytes = Uint8Array.from(atob(encoded), ch => ch.charCodeAt(0)); const text = new TextDecoder().decode(bytes). Node.js에서는 Buffer.from(encoded, 'base64').toString('utf8')을 사용하면 자동으로 처리됩니다.
JavaScript에서 JWT 토큰 페이로드를 디코딩하는 방법은 무엇인가요?
JWT는 점으로 구분된 세 개의 URL 안전 Base64 세그먼트로 구성됩니다: header.payload.signature. 페이로드를 디코딩하려면: const [, payloadB64] = token.split('.'). 브라우저에서: 표준 문자를 복원하고, 패딩을 추가하고, atob()와 TextDecoder로 디코딩합니다. Node.js 18+에서: Buffer.from(payloadB64, 'base64url').toString('utf8'). 중요: 디코딩은 클레임만 보여줄 뿐 서명을 검증하지 않습니다. 프로덕션에서 검증된 디코딩에는 JWT 라이브러리(jsonwebtoken, jose)를 사용하세요.
디코딩에서 atob()와 Buffer.from()의 차이점은 무엇인가요?
atob()는 모든 JavaScript 환경(브라우저, Node.js 16+, Bun, Deno)에서 import 없이 사용 가능하지만, 바이너리 문자열을 반환합니다 — UTF-8 콘텐츠를 읽을 수 있는 텍스트로 변환하려면 TextDecoder가 필요합니다. Buffer.from(encoded, 'base64')는 Node.js/Bun 전용이며, 실제 Buffer(바이너리 안전)를 반환하고, UTF-8을 네이티브로 처리하며, Node.js 18+에서 'base64url'을 지원합니다. 서버 사이드 코드에는 Buffer가 더 간단합니다. 브라우저 코드에는 atob() + TextDecoder가 표준입니다. 크로스 환경 라이브러리에는 js-base64가 차이를 추상화합니다.
브라우저에서 URL 안전 Base64를 디코딩하는 방법은 무엇인가요?
URL 안전 Base64는 +를 -로, /를 _로 바꾸고, = 패딩을 제거합니다. atob()를 호출하기 전에 복원합니다: const b64 = input.replace(/-/g, '+').replace(/_/g, '/'); const padded = b64 + '==='.slice(0, (4 - b64.length % 4) % 4); const text = new TextDecoder().decode(Uint8Array.from(atob(padded), c => c.charCodeAt(0))). Node.js 18+에서: Buffer.from(input, 'base64url').toString('utf8')으로 한 번의 호출로 처리됩니다.
JavaScript에서 GitHub API의 Base64 콘텐츠를 디코딩하는 방법은 무엇인가요?
GitHub Contents API는 60자마다 개행 문자가 있는 표준 Base64로 파일 콘텐츠를 반환합니다. 디코딩 전에 제거합니다: const clean = data.content.replace(/\n/g, ''). 브라우저에서: new TextDecoder().decode(Uint8Array.from(atob(clean), c => c.charCodeAt(0))). Node.js에서: Buffer.from(clean, 'base64').toString('utf8'). 바이너리 파일(이미지, PDF)의 경우 .toString()을 호출하지 말고 Buffer를 유지하세요 — writeFile이나 응답 스트림에 직접 전달하세요.
라이브러리 없이 브라우저에서 Base64 인코딩된 이미지를 디코딩할 수 있나요?
네. 브라우저에서: const binary = atob(encoded); const bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0)); const blob = new Blob([bytes], { type: 'image/png' }); const url = URL.createObjectURL(blob). img src의 경우 data URI를 구축하는 것이 더 간단합니다: const src = 'data:image/png;base64,' + encoded — 이는 디코딩 단계를 완전히 건너뜁니다. Node.js에서: Buffer.from(encoded, 'base64') 후 writeFileSync('./out.png', buffer). 핵심 규칙: 콘텐츠가 바이너리인 경우 디코딩된 Buffer에 절대 .toString()을 호출하지 마세요.

관련 도구

코드 작성 없이 원클릭 디코딩을 위해 Base64 문자열을 직접 Base64 Decoder 에 붙여넣으세요 — 브라우저에서 표준과 URL 안전 모드를 즉시 처리합니다.

다른 언어로도 제공됩니다:PythonGoJavaC#
AC
Alex ChenFront-end & Node.js Developer

Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.

SL
Sophie Laurent기술 검토자

Sophie is a full-stack developer focused on TypeScript across the entire stack — from React frontends to Express and Fastify backends. She has a particular interest in type-safe API design, runtime validation, and the patterns that make large JavaScript codebases stay manageable. She writes about TypeScript idioms, Node.js internals, and the ever-evolving JavaScript module ecosystem.