프론트엔드에서는 정상으로 보이던 쿼리 문자열이 백엔드에서 깨져 보이거나, URL에 공백·+·한글이 들어가는 순간 API가 400을 반환하는 경우가 있습니다. 이런 문제는 네트워크 장애가 아니라 URL 인코딩을 잘못된 지점에 적용했기 때문인 경우가 많습니다.
1. URL 인코딩이란 무엇인가
URL은 원래 사용할 수 있는 문자가 제한적입니다. 공백, 유니코드 문자, 이모지, 그리고 &, = 같은 구조 문자를 안전하게 전달하려면 퍼센트 인코딩(Percent-Encoding)이 필요합니다. 형식은 % + 2자리 16진수이며, 공백은 보통 %20으로 표현됩니다.
올바르게 인코딩하지 않으면 값 일부가 구분자로 해석되어 파라미터가 분리되거나 손상될 수 있습니다.
2. 어떤 문자를 인코딩해야 하나
실무에서는 다음 세 범주로 생각하면 명확합니다.
- Unreserved(대체로 그대로 사용 가능):
A-Z,a-z,0-9,-,_,.,~ - Reserved(구조 의미를 가짐):
:,/,?,#,[,],@,&,= - 비 ASCII 문자: UTF-8 바이트로 변환 후 퍼센트 인코딩 필요
핵심은 "전부 인코딩"이 아니라 "URL 컴포넌트별로 필요한 부분만 인코딩"입니다.
3. 공백이 %20일 때도 있고 +일 때도 있는 이유
일반적인 URL 퍼센트 인코딩에서는 공백이 %20입니다. 하지만 application/x-www-form-urlencoded 규칙(HTML 폼 전송)에서는 공백이 +로 표현됩니다.
한 시스템은 +를 공백으로, 다른 시스템은 문자 플러스로 해석하면 데이터 불일치가 생깁니다. 서비스 간 규약을 먼저 맞추는 것이 중요합니다.
4. JavaScript: encodeURI vs encodeURIComponent
둘 다 인코딩 함수지만 사용 목적이 다릅니다.
encodeURI: URL 전체용.:,/,?같은 구조 문자를 유지합니다.encodeURIComponent: 단일 파라미터 값용.&,=도 인코딩해 쿼리 구조 오염을 막습니다.
const keyword = 'C++ 입문 & 예제';
const url = '/search?q=' + encodeURIComponent(keyword);
// /search?q=C%2B%2B%20%EC%9E%85%EB%AC%B8%20%26%20%EC%98%88%EC%A0%9C
파라미터 값을 encodeURI로 처리하면 &가 새 파라미터 구분자로 해석될 수 있습니다.
5. 대표적 장애: 이중 인코딩(Double Encoding)
프론트엔드, SDK, 게이트웨이, 백엔드가 같은 값을 중복 인코딩하면 이중 인코딩이 발생합니다. 이때 %는 %25가 되어 데이터가 변형됩니다.
- 원본:
hello world - 1회 인코딩:
hello%20world - 2회 인코딩:
hello%2520world
설계 문서에 "누가 인코딩/디코딩을 담당하는지"를 경계별로 명시해야 합니다.
6. Path, Query, Fragment는 분리해서 처리
URL의 각 영역은 규칙이 다릅니다. URL 전체를 하나의 문자열로 처리하면 경로 슬래시가 깨지거나 쿼리 파싱 오류가 생깁니다.
| 영역 | 권장 방식 | 자주 하는 실수 |
|---|---|---|
| Path | 세그먼트 단위 인코딩, 라우팅 구조 유지 | 전체 경로를 한 번에 인코딩해 라우트 불일치 발생 |
| Query | 키와 값을 각각 인코딩 | 값 안의 &, = 미인코딩 |
| Fragment | 프런트 라우터 규칙에 맞춰 처리 | 서버 쿼리 규칙과 혼용 |
7. 보안 관점: 인코딩은 방어 그 자체가 아니다
URL 인코딩은 전송 표현을 안전하게 만드는 기술일 뿐, XSS/SQL Injection 방어를 대체하지 않습니다.
- 수신 후 값 검증(화이트리스트, 길이 제한)을 수행합니다.
- 출력 맥락(HTML/SQL/Shell)에 맞는 이스케이프를 적용합니다.
- 디코딩된 값을 DOM에 바로 삽입하지 않습니다.
결론
URL 인코딩의 핵심은 문자표 암기가 아니라 경계와 책임의 일관성입니다. 컴포넌트 단위 규칙을 팀에서 합의하면, 재현하기 어려운 API 버그를 크게 줄일 수 있습니다.