URL Encoding 완벽 이해 - 한글 깨짐과 특수문자 문제 해결
API 호출에서 한글이 깨지고, 특수문자가 오류를 일으키는 이유. encodeURI vs encodeURIComponent 차이와 실전 해결법.
Toolypet Team
Development Team
URL Encoding 완벽 이해
"API에 한글 파라미터 보냈더니 깨져서 왔어요."
보낸 것: ?name=홍길동
받은 것: ?name=í길ë
이 문제의 원인은 URL 인코딩입니다. 제대로 이해하면 다시는 깨진 글자로 고생하지 않습니다.
왜 인코딩이 필요한가
URL은 1990년대에 만들어졌고, ASCII 문자만 허용합니다.
URL에서 안전한 문자
A-Z a-z 0-9 - _ . ~
이게 전부입니다.
URL에서 문제가 되는 문자
| 문자 | 문제 |
|---|---|
| 한글 | ASCII 아님 |
| 공백 | URL 구분자로 해석 |
& | 파라미터 구분자 |
? | 쿼리스트링 시작 |
= | 키-값 구분자 |
# | fragment 시작 |
/ | 경로 구분자 |
인코딩이 하는 일
문제되는 문자를 %XX 형식(16진수)으로 변환합니다.
공백 → %20
& → %26
홍 → %ED%99%8D (UTF-8 기준 3바이트)
JavaScript 인코딩 함수
핵심 차이점
const text = "hello world&name=홍길동";
encodeURI(text);
// "hello%20world&name=%ED%99%8D%EA%B8%B8%EB%8F%99"
// & 와 = 는 그대로 (URL 구조 유지)
encodeURIComponent(text);
// "hello%20world%26name%3D%ED%99%8D%EA%B8%B8%EB%8F%99"
// & → %26, = → %3D (모두 인코딩)
언제 무엇을 쓰는가
| 상황 | 함수 | 이유 |
|---|---|---|
| 쿼리 파라미터 값 | encodeURIComponent | &, =도 인코딩해야 |
| 전체 URL | encodeURI | URL 구조 유지 |
| 경로 세그먼트 | encodeURIComponent | /도 인코딩해야 |
실수하기 쉬운 케이스
// ❌ 잘못된 사용: 전체 URL을 encodeURIComponent
const url = "https://api.com/search?q=테스트";
encodeURIComponent(url);
// "https%3A%2F%2Fapi.com%2Fsearch%3Fq%3D%ED%85%8C%EC%8A%A4%ED%8A%B8"
// URL로 사용 불가능!
// ✅ 올바른 사용: 값만 인코딩
const query = "테스트";
const url = `https://api.com/search?q=${encodeURIComponent(query)}`;
// "https://api.com/search?q=%ED%85%8C%EC%8A%A4%ED%8A%B8"
URLSearchParams - 현대적 방법
직접 인코딩하는 대신 URLSearchParams를 사용하세요.
기본 사용
const params = new URLSearchParams({
name: "홍길동",
city: "서울시 강남구",
tags: "개발자,프론트엔드"
});
params.toString();
// "name=%ED%99%8D%EA%B8%B8%EB%8F%99&city=%EC%84%9C%EC%9A%B8%EC%8B%9C+%EA%B0%95%EB%82%A8%EA%B5%AC&tags=%EA%B0%9C%EB%B0%9C%EC%9E%90%2C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C"
자동으로 인코딩됩니다. 실수할 일이 없습니다.
URL 객체와 함께
const url = new URL("https://api.com/search");
url.searchParams.set("q", "홍길동");
url.searchParams.set("page", 1);
url.toString();
// "https://api.com/search?q=%ED%99%8D%EA%B8%B8%EB%8F%99&page=1"
파싱 (디코딩)
const url = new URL("https://api.com/search?name=%ED%99%8D%EA%B8%B8%EB%8F%99");
url.searchParams.get("name"); // "홍길동" ← 자동 디코딩!
실전 문제 해결
문제 1: 이중 인코딩
// 이미 인코딩된 값을 또 인코딩
const encoded = "%ED%99%8D%EA%B8%B8%EB%8F%99";
encodeURIComponent(encoded);
// "%25ED%2599%258D%EA%B8%B8%EB%8F%99"
// % → %25 로 변환되어 완전히 깨짐
해결: 인코딩 전에 이미 인코딩되었는지 확인
function safeEncode(str) {
try {
// 이미 인코딩되었다면 디코딩 후 다시 인코딩
return encodeURIComponent(decodeURIComponent(str));
} catch {
// 디코딩 실패 = 인코딩 안 된 상태
return encodeURIComponent(str);
}
}
문제 2: 공백의 두 얼굴
%20 vs +
둘 다 공백이지만 출처가 다릅니다.
| 방식 | 사용처 |
|---|---|
%20 | URL 표준 (RFC 3986) |
+ | HTML 폼 (application/x-www-form-urlencoded) |
encodeURIComponent("hello world"); // "hello%20world"
new URLSearchParams({q: "hello world"}).toString(); // "q=hello+world"
둘 다 서버에서 올바르게 처리됩니다. 하지만 일관성을 위해 하나의 방식을 선택하세요.
문제 3: 프레임워크가 이미 인코딩함
axios, fetch, React Router 등은 자동으로 인코딩합니다.
// axios - 자동 인코딩
axios.get('/search', { params: { name: '홍길동' } });
// 요청: /search?name=%ED%99%8D%EA%B8%B8%EB%8F%99
// ❌ 잘못: 직접 인코딩하면 이중 인코딩
axios.get('/search', { params: { name: encodeURIComponent('홍길동') } });
// 요청: /search?name=%25ED%2599%258D... (깨짐)
규칙: 프레임워크를 사용한다면 직접 인코딩하지 마세요.
서버 사이드 처리
Node.js / Express
// 자동 디코딩됨
app.get('/search', (req, res) => {
const name = req.query.name; // "홍길동"
});
// 경로 파라미터도 마찬가지
app.get('/users/:name', (req, res) => {
const name = req.params.name; // "홍길동"
});
Python / Flask
from flask import request
@app.route('/search')
def search():
name = request.args.get('name') # "홍길동"
PHP
$name = $_GET['name']; // "홍길동"
// PHP도 자동 디코딩
특수 케이스
Base64 in URL
표준 Base64에는 +, /, =가 있어 URL에서 문제됩니다.
// 표준 Base64
btoa("hello"); // "aGVsbG8="
// URL-safe Base64
function base64UrlEncode(str) {
return btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, ''); // padding 제거
}
base64UrlEncode("hello"); // "aGVsbG8"
JWT가 이 형식을 사용합니다.
Fragment (#) 주의
https://example.com/page?name=홍길동#section
↑
여기부터 서버로 안 감
# 이후(fragment)는 브라우저에서만 처리되고 서버로 전송되지 않습니다.
디버깅 팁
브라우저 주소창
브라우저는 가독성을 위해 디코딩해서 보여줍니다.
표시: https://example.com/users/홍길동
실제: https://example.com/users/%ED%99%8D%EA%B8%B8%EB%8F%99
개발자 도구 Network 탭에서 실제 요청 URL을 확인하세요.
curl로 테스트
# 직접 인코딩된 URL
curl "https://api.com/search?name=%ED%99%8D%EA%B8%B8%EB%8F%99"
# curl이 인코딩하게
curl -G "https://api.com/search" --data-urlencode "name=홍길동"
요약
| 상황 | 해결 |
|---|---|
| 쿼리 파라미터 만들기 | URLSearchParams 사용 |
| 값만 인코딩 | encodeURIComponent |
| 전체 URL 인코딩 | encodeURI |
| 프레임워크 사용 중 | 직접 인코딩 하지 않기 |
| 깨진 글자 | 이중 인코딩 의심 |
핵심 원칙: 가능하면 URLSearchParams나 프레임워크에 맡기고, 직접 인코딩은 피하세요.
관련 도구
| 도구 | 용도 |
|---|---|
| URL Encoder | 인코딩/디코딩 |
| URL Parser | URL 구조 분석 |
| Base64 | Base64 변환 |
저자 소개
Toolypet Team
Development Team
The Toolypet Team creates free, privacy-focused web tools for developers and designers. All tools run entirely in your browser with no data sent to servers.