URL 인코딩 완벽 설명 - 알아야 할 모든 것

URL 인코딩 완벽 설명 - 알아야 할 모든 것

URL 인코딩(퍼센트 인코딩)의 원리와 실무 활용법을 완벽히 설명합니다. 예약 문자, UTF-8 인코딩, encodeURIComponent, 실용적 예제, 자주 발생하는 문제와 해결법까지 개발자가 알아야 할 모든 것을 다룹니다.

2026년 3월 8일11분 소요

URL 인코딩 완벽 설명 - 알아야 할 모든 것

URL(Uniform Resource Locator)은 인터넷에서 리소스의 위치를 지정하는 주소 체계입니다. 우리가 매일 사용하는 웹 주소가 바로 URL인데, 이 URL에는 사용할 수 있는 문자에 대한 엄격한 규칙이 있습니다. URL 인코딩(또는 퍼센트 인코딩)은 URL에서 안전하게 사용할 수 없는 문자를 특수한 형태로 변환하는 과정입니다.

개발자라면 API 호출, 쿼리 파라미터 처리, 리다이렉트 URL 구성 등 다양한 상황에서 URL 인코딩을 다루게 됩니다. 올바르게 이해하지 못하면 깨진 링크, 보안 취약점, 데이터 손실 등의 문제가 발생할 수 있습니다. 이 가이드에서는 URL 인코딩의 원리부터 실무 활용법까지 모든 것을 상세히 설명합니다. 직접 URL 인코딩/디코딩을 테스트해보고 싶다면 URL 인코더/디코더 도구를 활용해 보세요.

URL의 구조 이해하기

URL 인코딩을 이해하려면 먼저 URL의 구조를 알아야 합니다.

https://user:password@www.example.com:8080/path/to/page?query=value&key=value#fragment
\___/   \__________/ \_______________/ \__/\_________/ \___________________/ \______/
  |          |              |            |       |              |                |
스킴     사용자정보        호스트        포트    경로          쿼리             프래그먼트
(scheme) (userinfo)      (host)       (port)  (path)        (query)          (fragment)

각 구성 요소마다 허용되는 문자와 인코딩 규칙이 다릅니다. 이를 이해하는 것이 올바른 URL 인코딩의 핵심입니다.

URL 구성 요소별 설명

구성 요소예시설명
스킴(Scheme)https프로토콜 (http, https, ftp 등)
사용자정보(Userinfo)user:password인증 정보 (거의 사용하지 않음)
호스트(Host)www.example.com도메인 또는 IP 주소
포트(Port)8080서버 포트 번호 (기본값: 80/443)
경로(Path)/path/to/page리소스 경로
쿼리(Query)query=value키-값 쌍의 파라미터
프래그먼트(Fragment)section1페이지 내 앵커 위치

퍼센트 인코딩의 원리

퍼센트 인코딩(Percent-encoding)은 URL에서 특수한 의미를 가지거나 허용되지 않는 문자를 % 뒤에 16진수 두 자리로 표현하는 방식입니다.

인코딩 과정

  1. 인코딩할 문자를 UTF-8로 변환하여 바이트 시퀀스를 얻습니다
  2. 각 바이트를 %XX 형식(XX는 16진수)으로 변환합니다
문자 "한" → UTF-8 바이트: 0xED 0x95 0x9C → 인코딩: %ED%95%9C
문자 " " (공백) → UTF-8 바이트: 0x20 → 인코딩: %20
문자 "&" → UTF-8 바이트: 0x26 → 인코딩: %26

예약 문자 (Reserved Characters)

URL에서 특별한 의미를 가지는 문자들입니다. 이 문자들을 데이터의 일부로 사용하려면 반드시 인코딩해야 합니다.

문자인코딩URL에서의 역할
:%3A스킴과 호스트 구분, 포트 지정
/%2F경로 구분자
?%3F쿼리 문자열 시작
#%23프래그먼트 시작
[%5BIPv6 주소 표기
]%5DIPv6 주소 표기
@%40사용자정보와 호스트 구분
!%21하위 구분자
$%24하위 구분자
&%26쿼리 파라미터 구분
'%27하위 구분자
(%28하위 구분자
)%29하위 구분자
*%2A하위 구분자
+%2B쿼리에서 공백 (역사적 이유)
,%2C하위 구분자
;%3B파라미터 구분자
=%3D쿼리에서 키-값 구분

비예약 문자 (Unreserved Characters)

다음 문자들은 인코딩 없이 URL에서 안전하게 사용할 수 있습니다:

A-Z a-z 0-9 - _ . ~

이 외의 모든 문자(한글, 중국어, 일본어, 공백, 특수문자 등)는 퍼센트 인코딩이 필요합니다.

URL에서의 UTF-8 인코딩

현대 웹에서 URL의 비ASCII 문자는 UTF-8로 인코딩한 후 퍼센트 인코딩을 적용합니다. 이는 RFC 3986과 WHATWG URL 표준에서 규정하고 있습니다.

한글 인코딩 예시

"서울" → UTF-8 바이트:
  "서" → 0xEC 0x84 0x9C → %EC%84%9C
  "울" → 0xEC%9A%B8 → %EC%9A%B8

결과: %EC%84%9C%EC%9A%B8

다양한 언어의 인코딩

원본 텍스트UTF-8 인코딩 결과
서울%EC%84%9C%EC%9A%B8
東京%E6%9D%B1%E4%BA%AC
cafecafe (ASCII는 인코딩 불필요)
cafe (악센트 포함)caf%C3%A9
2026년2026%EB%85%84

JavaScript에서의 URL 인코딩

JavaScript는 URL 인코딩을 위한 여러 내장 함수를 제공합니다. 각 함수의 차이를 정확히 이해하는 것이 중요합니다.

encodeURIComponent vs encodeURI

const text = "검색어=Hello World&page=1";

// encodeURIComponent - 쿼리 파라미터 값에 사용
console.log(encodeURIComponent(text));
// 결과: %EA%B2%80%EC%83%89%EC%96%B4%3DHello%20World%26page%3D1
// &, = 등 예약 문자도 모두 인코딩

// encodeURI - 전체 URL에 사용
console.log(encodeURI(text));
// 결과: %EA%B2%80%EC%83%89%EC%96%B4=Hello%20World&page=1
// &, = 등 URL 구조 문자는 인코딩하지 않음

인코딩 함수 비교표

문자encodeURIComponentencodeURIescape (비권장)
공백%20%20%20
!!!%21
#%23#%23
$%24$%24
&%26&%26
+%2B++
/%2F//
:%3A:%3A
=%3D=%3D
?%3F?%3F
@%40@%40
한글%EC%84%9C%EC%84%9C%uC11C (비표준)

올바른 사용법

// 1. 쿼리 파라미터 값 인코딩 - encodeURIComponent 사용
const searchQuery = "C++ 프로그래밍 & 알고리즘";
const url1 = `https://example.com/search?q=${encodeURIComponent(searchQuery)}`;
// https://example.com/search?q=C%2B%2B%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%20%26%20%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98

// 2. URLSearchParams 사용 (권장)
const params = new URLSearchParams({
  q: "C++ 프로그래밍 & 알고리즘",
  page: "1",
  sort: "relevance",
});
const url2 = `https://example.com/search?${params.toString()}`;
// https://example.com/search?q=C%2B%2B+%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D+%26+%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98&page=1&sort=relevance

// 3. URL 객체 사용 (가장 안전)
const url3 = new URL('https://example.com/search');
url3.searchParams.set('q', 'C++ 프로그래밍 & 알고리즘');
url3.searchParams.set('page', '1');
console.log(url3.toString());
// https://example.com/search?q=C%2B%2B+%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D+%26+%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98&page=1

// 4. 디코딩
console.log(decodeURIComponent('%EC%84%9C%EC%9A%B8'));
// 서울

console.log(decodeURI('https://example.com/%EC%84%9C%EC%9A%B8'));
// https://example.com/서울

URL 경로 인코딩

// 경로의 각 세그먼트를 개별적으로 인코딩해야 합니다
const category = "개발자 도구";
const subcategory = "JSON/XML";

// 잘못된 방법 - 슬래시까지 인코딩됨
const wrongUrl = `https://example.com/${encodeURIComponent(`${category}/${subcategory}`)}`;
// https://example.com/%EA%B0%9C%EB%B0%9C%EC%9E%90%20%EB%8F%84%EA%B5%AC%2FJSON%2FXML

// 올바른 방법 - 각 세그먼트를 개별 인코딩
const correctUrl = `https://example.com/${encodeURIComponent(category)}/${encodeURIComponent(subcategory)}`;
// https://example.com/%EA%B0%9C%EB%B0%9C%EC%9E%90%20%EB%8F%84%EA%B5%AC/JSON%2FXML

다른 프로그래밍 언어에서의 URL 인코딩

Python

from urllib.parse import quote, quote_plus, unquote, urlencode

# 기본 인코딩 (경로용)
print(quote("서울/강남구"))
# %EC%84%9C%EC%9A%B8/%EA%B0%95%EB%82%A8%EA%B5%AC
# 참고: 슬래시는 기본적으로 인코딩하지 않음

# 전체 인코딩 (파라미터 값용)
print(quote("서울/강남구", safe=""))
# %EC%84%9C%EC%9A%B8%2F%EA%B0%95%EB%82%A8%EA%B5%AC

# 공백을 +로 인코딩 (폼 데이터용)
print(quote_plus("Hello World"))
# Hello+World

# 쿼리 문자열 생성
params = {"검색어": "파이썬 프로그래밍", "페이지": "1"}
print(urlencode(params))
# %EA%B2%80%EC%83%89%EC%96%B4=%ED%8C%8C%EC%9D%B4%EC%8D%AC+%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&%ED%8E%98%EC%9D%B4%EC%A7%80=1

# 디코딩
print(unquote("%EC%84%9C%EC%9A%B8"))
# 서울

Java

import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

// 인코딩
String encoded = URLEncoder.encode("서울 강남구", StandardCharsets.UTF_8);
System.out.println(encoded);
// %EC%84%9C%EC%9A%B8+%EA%B0%95%EB%82%A8%EA%B5%AC
// 주의: 공백이 +로 인코딩됨

// %20으로 공백을 인코딩하려면
String encodedWithPercent = URLEncoder.encode("서울 강남구", StandardCharsets.UTF_8)
    .replace("+", "%20");
System.out.println(encodedWithPercent);
// %EC%84%9C%EC%9A%B8%20%EA%B0%95%EB%82%A8%EA%B5%AC

// 디코딩
String decoded = URLDecoder.decode("%EC%84%9C%EC%9A%B8", StandardCharsets.UTF_8);
System.out.println(decoded);
// 서울

Go

package main

import (
    "fmt"
    "net/url"
)

func main() {
    // 경로 인코딩
    encoded := url.PathEscape("서울/강남구")
    fmt.Println(encoded)
    // %EC%84%9C%EC%9A%B8%2F%EA%B0%95%EB%82%A8%EA%B5%AC

    // 쿼리 파라미터 인코딩
    queryEncoded := url.QueryEscape("검색어=파이썬&페이지=1")
    fmt.Println(queryEncoded)
    // %EA%B2%80%EC%83%89%EC%96%B4%3D%ED%8C%8C%EC%9D%B4%EC%8D%AC%26%ED%8E%98%EC%9D%B4%EC%A7%80%3D1

    // URL 객체 사용 (권장)
    u, _ := url.Parse("https://example.com/search")
    q := u.Query()
    q.Set("q", "Go 프로그래밍")
    q.Set("page", "1")
    u.RawQuery = q.Encode()
    fmt.Println(u.String())
    // https://example.com/search?page=1&q=Go+%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D
}

공백 인코딩: %20 vs +

URL 인코딩에서 가장 혼란스러운 부분 중 하나가 공백의 처리입니다.

두 가지 표준

상황인코딩 방식표준
URL 경로%20RFC 3986
쿼리 문자열+ 또는 %20application/x-www-form-urlencoded
HTML 폼 데이터+HTML 표준
REST API 파라미터%20 권장관례
// 공백 인코딩 차이 예시
const text = "Hello World";

// encodeURIComponent: 항상 %20 사용
console.log(encodeURIComponent(text));
// Hello%20World

// URLSearchParams: + 사용 (application/x-www-form-urlencoded)
const params = new URLSearchParams({ q: text });
console.log(params.toString());
// q=Hello+World

// 서버에서는 두 가지 모두 올바르게 디코딩해야 함

실용적 예제

1. API 호출에서의 URL 인코딩

// REST API 호출 시 URL 인코딩
async function searchProducts(query, filters) {
  const url = new URL('https://api.example.com/v1/products');

  // searchParams를 사용하면 자동으로 인코딩됨
  url.searchParams.set('q', query);
  url.searchParams.set('category', filters.category);
  url.searchParams.set('price_range', `${filters.minPrice}-${filters.maxPrice}`);
  url.searchParams.set('sort', filters.sort);

  const response = await fetch(url.toString(), {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Accept': 'application/json',
    },
  });

  return response.json();
}

// 사용 예시
searchProducts('개발자 노트북', {
  category: '전자기기/노트북',
  minPrice: 1000000,
  maxPrice: 3000000,
  sort: '가격순',
});

// 생성되는 URL:
// https://api.example.com/v1/products?q=%EA%B0%9C%EB%B0%9C%EC%9E%90+%EB%85%B8%ED%8A%B8%EB%B6%81&category=%EC%A0%84%EC%9E%90%EA%B8%B0%EA%B8%B0%2F%EB%85%B8%ED%8A%B8%EB%B6%81&price_range=1000000-3000000&sort=%EA%B0%80%EA%B2%A9%EC%88%9C

2. 리다이렉트 URL 인코딩

// OAuth 콜백에서의 리다이렉트 URL
function buildLoginUrl(redirectAfterLogin) {
  const loginUrl = new URL('https://auth.example.com/login');

  // 리다이렉트 URL 자체를 파라미터 값으로 인코딩
  loginUrl.searchParams.set('redirect_uri', redirectAfterLogin);
  loginUrl.searchParams.set('response_type', 'code');
  loginUrl.searchParams.set('client_id', 'my-app');

  return loginUrl.toString();
}

const url = buildLoginUrl('https://myapp.com/callback?state=abc&type=login');
// https://auth.example.com/login?redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback%3Fstate%3Dabc%26type%3Dlogin&response_type=code&client_id=my-app

// 이중 인코딩 주의!
// URLSearchParams가 이미 인코딩하므로 encodeURIComponent를 또 사용하면 안 됨

3. 파일명 인코딩

// 한글 파일명이 포함된 다운로드 URL
function getDownloadUrl(filename) {
  const encodedFilename = encodeURIComponent(filename);
  return `https://storage.example.com/files/${encodedFilename}`;
}

console.log(getDownloadUrl("보고서_2026년_3월.pdf"));
// https://storage.example.com/files/%EB%B3%B4%EA%B3%A0%EC%84%9C_2026%EB%85%84_3%EC%9B%94.pdf

// Content-Disposition 헤더에서의 파일명 인코딩
function setDownloadHeaders(res, filename) {
  const encodedFilename = encodeURIComponent(filename);
  res.setHeader(
    'Content-Disposition',
    `attachment; filename="${encodedFilename}"; filename*=UTF-8''${encodedFilename}`
  );
}

자주 발생하는 문제와 해결법

1. 이중 인코딩 (Double Encoding)

// 문제: 이미 인코딩된 문자열을 다시 인코딩
const originalUrl = "https://example.com/검색";
const encoded = encodeURI(originalUrl);
// https://example.com/%EA%B2%80%EC%83%89

// 이중 인코딩 발생!
const doubleEncoded = encodeURI(encoded);
// https://example.com/%25EA%25B2%2580%25EC%2583%2589
// %가 %25로 다시 인코딩됨

// 해결: 인코딩 전에 먼저 디코딩
function safeEncode(url) {
  try {
    // 이미 인코딩되어 있다면 먼저 디코딩
    const decoded = decodeURI(url);
    return encodeURI(decoded);
  } catch (e) {
    return encodeURI(url);
  }
}

2. 잘못된 디코딩

// 문제: decodeURIComponent에 유효하지 않은 인코딩 전달
try {
  // % 뒤에 유효한 16진수가 없는 경우
  decodeURIComponent('%E0%A4%A');
  // URIError: URI malformed
} catch (e) {
  console.error('디코딩 실패:', e.message);
}

// 안전한 디코딩 함수
function safeDecode(str) {
  try {
    return decodeURIComponent(str);
  } catch (e) {
    // 디코딩 실패 시 원본 반환
    console.warn(`디코딩 실패: ${str}`);
    return str;
  }
}

3. 인코딩 방식 혼합

// 문제: EUC-KR로 인코딩된 한글을 UTF-8로 디코딩 시도
// 레거시 시스템에서 자주 발생

// 해결: 올바른 인코딩 방식 확인 후 처리
// Node.js에서 iconv-lite 사용
import iconv from 'iconv-lite';

function decodeEucKr(buffer) {
  return iconv.decode(buffer, 'euc-kr');
}

// 웹에서 TextDecoder 사용
const decoder = new TextDecoder('euc-kr');
const decoded = decoder.decode(uint8Array);

URL 인코딩과 보안

인코딩 기반 공격 방어

URL 인코딩은 보안 관점에서도 중요합니다. 공격자는 인코딩을 이용하여 보안 필터를 우회하려 시도할 수 있습니다.

// 경로 순회 공격 방지
function sanitizePath(userInput) {
  // 1단계: 디코딩 (이중 인코딩 공격 방지)
  let decoded = userInput;
  let previous;
  do {
    previous = decoded;
    try {
      decoded = decodeURIComponent(decoded);
    } catch (e) {
      break;
    }
  } while (decoded !== previous);

  // 2단계: 경로 순회 패턴 제거
  const sanitized = decoded
    .replace(/\.\./g, '')
    .replace(/\/\//g, '/');

  // 3단계: 허용된 문자만 허용
  if (!/^[a-zA-Z0-9가-힣_\-\/\.]+$/.test(sanitized)) {
    throw new Error('잘못된 경로입니다.');
  }

  return sanitized;
}

// Open Redirect 방지
function validateRedirectUrl(url) {
  try {
    const parsed = new URL(url);
    const allowedHosts = ['toolboxhubs.com', 'www.toolboxhubs.com'];

    if (!allowedHosts.includes(parsed.hostname)) {
      throw new Error('허용되지 않은 리다이렉트 대상입니다.');
    }

    return parsed.toString();
  } catch (e) {
    throw new Error('유효하지 않은 URL입니다.');
  }
}

국제화 도메인 이름 (IDN)

한글 도메인과 같은 국제화 도메인 이름은 Punycode로 인코딩됩니다.

// IDN과 Punycode
const koreanDomain = "한국인터넷진흥원.한국";

// Punycode 변환
const url = new URL(`https://${koreanDomain}`);
console.log(url.hostname);
// xn--lg3bt6bm1dl0r6wp7eb5o.xn--3e0b707e

// URL API는 자동으로 변환을 처리합니다
console.log(url.toString());
// https://xn--lg3bt6bm1dl0r6wp7eb5o.xn--3e0b707e/

URL 인코딩 참조표

자주 사용되는 문자의 인코딩 결과를 정리한 참조표입니다.

문자인코딩문자인코딩
공백%20<%3C
!%21>%3E
"%22{%7B
#%23}%7D
$%24|%7C
%%25\%5C
&%26^%5E
'%27`%60
+%2B~%7E
,%2C%09
/%2F줄바꿈%0A

관련 도구 활용

URL 인코딩 작업을 할 때 다음 온라인 도구들이 유용합니다:

결론

URL 인코딩은 웹 개발의 기본이면서도 종종 간과되는 영역입니다. 올바른 URL 인코딩은 애플리케이션의 안정성과 보안 모두에 직접적인 영향을 미칩니다.

핵심 원칙을 정리하면:

  1. 쿼리 파라미터 값에는 encodeURIComponent를 사용하세요 (또는 URLSearchParams/URL 객체 활용)
  2. 전체 URL에는 encodeURI를 사용하세요
  3. 이중 인코딩을 주의하세요 - 이미 인코딩된 문자열을 다시 인코딩하지 마세요
  4. URL 객체나 URLSearchParams를 적극 활용하세요 - 수동 인코딩보다 안전합니다
  5. 보안을 위해 서버 측에서 반드시 디코딩 후 검증하세요

URL 인코더/디코더 도구를 사용하면 인코딩 결과를 바로 확인할 수 있으니, URL 인코딩 관련 작업 시 적극 활용해 보세요. 올바른 URL 인코딩 습관을 들이면 깨진 링크, 데이터 손실, 보안 취약점 등의 문제를 미연에 방지할 수 있습니다.

관련 글