URL 인코딩 완벽 설명 - 알아야 할 모든 것
URL 인코딩 완벽 설명 - 알아야 할 모든 것
URL 인코딩(퍼센트 인코딩)의 원리와 실무 활용법을 완벽히 설명합니다. 예약 문자, UTF-8 인코딩, encodeURIComponent, 실용적 예제, 자주 발생하는 문제와 해결법까지 개발자가 알아야 할 모든 것을 다룹니다.
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진수 두 자리로 표현하는 방식입니다.
인코딩 과정
- 인코딩할 문자를 UTF-8로 변환하여 바이트 시퀀스를 얻습니다
- 각 바이트를
%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 | 프래그먼트 시작 |
[ | %5B | IPv6 주소 표기 |
] | %5D | IPv6 주소 표기 |
@ | %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 |
| cafe | cafe (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 구조 문자는 인코딩하지 않음
인코딩 함수 비교표
| 문자 | encodeURIComponent | encodeURI | escape (비권장) |
|---|---|---|---|
| 공백 | %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 경로 | %20 | RFC 3986 |
| 쿼리 문자열 | + 또는 %20 | application/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 인코딩/디코딩을 즉시 수행
- Base64 인코더/디코더 - URL-safe Base64 인코딩
- JSON 포맷터 - API 응답에서 URL 파라미터 확인
- 해시 생성기 - URL 기반 캐시 키 생성
결론
URL 인코딩은 웹 개발의 기본이면서도 종종 간과되는 영역입니다. 올바른 URL 인코딩은 애플리케이션의 안정성과 보안 모두에 직접적인 영향을 미칩니다.
핵심 원칙을 정리하면:
- 쿼리 파라미터 값에는
encodeURIComponent를 사용하세요 (또는 URLSearchParams/URL 객체 활용) - 전체 URL에는
encodeURI를 사용하세요 - 이중 인코딩을 주의하세요 - 이미 인코딩된 문자열을 다시 인코딩하지 마세요
- URL 객체나 URLSearchParams를 적극 활용하세요 - 수동 인코딩보다 안전합니다
- 보안을 위해 서버 측에서 반드시 디코딩 후 검증하세요
URL 인코더/디코더 도구를 사용하면 인코딩 결과를 바로 확인할 수 있으니, URL 인코딩 관련 작업 시 적극 활용해 보세요. 올바른 URL 인코딩 습관을 들이면 깨진 링크, 데이터 손실, 보안 취약점 등의 문제를 미연에 방지할 수 있습니다.