한글 파라미터 넣었더니 %EC%95%88... 이게 뭐야 — URL 인코딩 5분 만에 이해하기
URL 인코딩이 뭔지, 왜 한글이 %XX로 바뀌는지, encodeURI와 encodeURIComponent 차이, 공백이 %20인지 +인지 헷갈리는 문제까지 한번에 정리했어요.
프론트엔드 개발 2년 차 때 일이에요. 검색 기능에 한글 키워드를 넣으면 API가 에러를 뱉는다는 QA 보고가 올라왔어요. 브라우저 개발자 도구 네트워크 탭을 열어보니 요청 URL에 '서울 맛집'이 그대로 박혀 있었어요. 공백도 있고 한글도 있고, 인코딩은 전혀 안 된 상태로요. 수정은 encodeURIComponent() 하나면 끝이었는데, 그걸 모르고 세 시간 동안 다른 데를 팠어요.
URL 인코딩은 한번 제대로 이해하면 다시는 막히지 않아요. 코드 한 줄이면 끝나거든요. 복잡하게 생각할 거 없어요. 지금부터 딱 필요한 것만 정리해드릴게요.
이 글에서 알 수 있는 것
- ✅한글이 왜 %EC%95%88 같은 문자열로 바뀌는지, 그 원리를 알 수 있어요
- ✅encodeURI와 encodeURIComponent 중 언제 뭘 써야 하는지 헷갈리지 않게 정리돼 있어요
- ✅이중 인코딩 함정을 포함해서, 실제로 많이 하는 실수 다섯 가지와 해결법을 알 수 있어요
URL 인코딩이 뭔가요?
URL에는 쓸 수 있는 문자가 정해져 있어요. 영문자, 숫자, 그리고 -, _, ., ~ 네 가지 특수문자만 그냥 써도 돼요. 그 외의 문자, 그러니까 한글, 공백, &, = 같은 건 퍼센트 기호와 16진수 조합으로 변환해야 브라우저랑 서버가 제대로 이해해요. 이걸 URL 인코딩 또는 퍼센트 인코딩이라고 해요.
원리는 간단해요. 문자를 UTF-8 바이트로 변환하고, 각 바이트를 %XX 형태로 써주면 돼요. 공백은 1바이트라서 %20이 되고, 한글 '가'는 UTF-8로 3바이트라서 %EA%B0%80이 돼요. 한글 한 글자에 % 세 개가 붙는 이유가 여기 있어요.
- 공백 → %20 (HTML 폼에서는 + — 이게 왜 헷갈리는지는 아래에서 설명해요)
- & → %26 (쿼리 스트링 구분자로 예약된 문자)
- = → %3D (key=value 할당에 쓰이는 예약 문자)
- / → %2F (경로 구분자로 예약된 문자)
- ? → %3F (쿼리 스트링 시작을 나타내는 예약 문자)
- # → %23 (프래그먼트 식별자)
- 한글, 이모지, 일본어 등 → 바이트마다 %XX 하나씩, 글자당 3~4개
지금 이 도구를 사용해 보세요:
URL 인코더/디코더 사용해보기 →encodeURI vs encodeURIComponent — 뭐가 다른 거예요?
자바스크립트에 URL 인코딩 함수가 두 개 있어요. encodeURI()랑 encodeURIComponent()예요. 이름만 보면 비슷해 보이는데, 쓰는 상황이 완전히 달라요. 이 차이를 모르면 인코딩을 했는데도 URL이 깨지는 황당한 상황을 만나요.
encodeURI()는 URL 전체를 넘길 때 써요. URL의 구조적인 문자인 /, ?, &, =, # 같은 건 건드리지 않아요. 이 문자들이 URL에서 의미를 가지고 있으니까요. encodeURIComponent()는 URL 안에 들어갈 '값' 하나를 인코딩할 때 써요. &와 = 같은 구분자도 다 인코딩해버려요. 쿼리 파라미터 값에 &가 들어가면 URL 구조 자체가 깨질 수 있으니까요.
// encodeURI — URL 전체를 넘길 때
encodeURI('https://example.com/search?q=서울 맛집&page=2')
// → 'https://example.com/search?q=%EC%84%9C%EC%9A%B8%20%EB%A7%9B%EC%A7%91&page=2'
// 한글이랑 공백은 인코딩됐고, &랑 =는 그대로예요
// encodeURIComponent — 쿼리 파라미터 '값'을 넣을 때
encodeURIComponent('서울 맛집&page=2')
// → '%EC%84%9C%EC%9A%B8%20%EB%A7%9B%EC%A7%91%26page%3D2'
// &가 %26이 됐어요. 값 안에 & 있어도 URL 구조 안 깨져요
// 실제로 URL 만들 때는 이렇게
const keyword = '서울 맛집';
const url = `https://api.example.com/search?q=${encodeURIComponent(keyword)}`;| 비교 항목 | encodeURI() | encodeURIComponent() |
|---|---|---|
| 쓰는 상황 | URL 전체를 인코딩할 때 | 쿼리 값 하나를 URL에 넣을 때 |
| 공백 인코딩 | 예 → %20 | 예 → %20 |
| & 인코딩 | 아니요 (그대로) | 예 → %26 |
| = 인코딩 | 아니요 (그대로) | 예 → %3D |
| / 인코딩 | 아니요 (그대로) | 예 → %2F |
| ? 인코딩 | 아니요 (그대로) | 예 → %3F |
| 한글 인코딩 | 예 | 예 |
공백은 %20이에요, +예요?
이거 헷갈리는 사람이 많아요. 정답은 '상황에 따라 다르다'인데요, 실무에서는 단순하게 생각하면 돼요. URL 경로나 쿼리 스트링에서 공백의 표준 인코딩은 %20이에요. encodeURIComponent()도 %20을 만들어요. 그런데 HTML 폼을 submit하면 브라우저가 폼 데이터를 application/x-www-form-urlencoded 형식으로 전송하는데, 이때 공백을 +로 인코딩해요. 오래된 HTTP 폼 방식에서 내려온 관습이에요.
내가 만드는 URL에는 무조건 %20, + 쓰지 마세요
프로그래밍으로 URL 만들 때는 encodeURIComponent()만 쓰면 돼요. 알아서 %20을 써줘요. +는 HTML 폼이 만들어내는 레거시 형식이에요. 직접 생산하면 다른 시스템에서 파싱할 때 문제가 생길 수 있어요.
이중 인코딩 함정 — 가장 많이 빠지는 실수
이중 인코딩이란 이미 인코딩된 문자열을 다시 인코딩하는 거예요. %20이 %2520이 돼버리는 상황이에요. %25가 퍼센트 기호(%) 자체의 인코딩이거든요. 서버에서 %2520을 받으면 %20이라는 문자열로 디코딩해요. 공백이 아니라 '%20'이라는 텍스트가 되는 거죠. 데이터가 깨지는 거예요.
// 잘못된 예: 이미 인코딩된 값을 또 인코딩하면
const alreadyEncoded = 'hello%20world';
const broken = encodeURIComponent(alreadyEncoded);
// → 'hello%2520world' %20의 %가 %25로 인코딩됨
// 올바른 방법: 먼저 디코딩하고 다시 인코딩
const decoded = decodeURIComponent(alreadyEncoded); // → 'hello world'
const correct = encodeURIComponent(decoded); // → 'hello%20world'
function safeEncode(input) {
try {
const decoded = decodeURIComponent(input);
return encodeURIComponent(decoded);
} catch {
return encodeURIComponent(input);
}
}이중 인코딩 — 이럴 때 특히 조심하세요
사용자 입력, DB에서 가져온 값, 외부 API 응답 등 이미 어딘가를 거친 데이터를 URL에 넣을 때 이중 인코딩이 일어나요. 이 값이 혹시 이미 인코딩된 건 아닐까? 항상 먼저 물어보세요. 의심스럽다면 decodeURIComponent()로 한번 디코딩하고 나서 인코딩하세요. 인코딩 함수를 겹쳐 쓰지 마세요.
URL 인코딩 실수 TOP 5
- 쿼리 값에 encodeURI() 쓰기: &랑 =를 인코딩 안 해서 쿼리 스트링 구조가 조용히 깨져요. 값에는 무조건 encodeURIComponent()예요.
- 전체 URL에 encodeURIComponent() 쓰기: ://, /, ?, &가 전부 인코딩돼서 URL이 완전히 박살나요. 전체 URL엔 encodeURI()예요.
- 인코딩 자체를 생략하기: 브라우저가 알아서 해주겠지 싶은데, 서버나 API는 그렇지 않아요. 특히 한글이나 특수문자가 섞인 경우 에러가 나거나 데이터가 깨져요. 보안 취약점으로 이어지기도 해요.
- 경로 세그먼트에 동적 값 그냥 넣기: /users/홍길동/profile 같은 경로를 그냥 쓰면 서버마다 동작이 달라요. 동적 부분은 encodeURIComponent()로 인코딩하고, / 구분자는 건드리지 마세요.
- 어떤 건 인코딩하고 어떤 건 안 하기: 일관성 없는 인코딩은 특정 입력에서만 터지는 버그를 만들어요. 동적 값은 항상, 모두 인코딩하는 게 원칙이에요.
자주 묻는 질문
공백이 %20이에요, +예요?
URL에 직접 쓸 때는 %20이 맞아요. HTML 폼이 submit할 때 application/x-www-form-urlencoded 형식으로 보내면 공백이 +로 인코딩돼요. 코드에서 encodeURIComponent()를 쓰면 무조건 %20이 나오니까, 직접 URL 만들 때는 %20으로 통일하면 돼요.
모든 문자를 인코딩해야 하나요?
아니에요. 영문자, 숫자, 그리고 -, _, ., ~ 네 가지 기호는 인코딩 없이 그냥 써도 돼요. /, ?, #, &, = 같은 예약 문자는 URL 구조에서 자기 역할로 쓸 때는 인코딩 안 해도 되고, 쿼리 값 안에 글자로 넣을 때만 인코딩해요. 그 외 한글, 공백, 나머지 특수문자는 인코딩해야 해요.
한글이 왜 %EC%95%88 같은 길고 복잡한 코드로 바뀌나요?
한글 한 글자가 UTF-8 인코딩으로 보통 3바이트예요. 각 바이트를 %XX로 표현하니까 글자 하나에 % 세 개가 붙어요. '안'은 0xEC, 0x95, 0x88이니까 %EC%95%88이 되는 거예요. '안녕하세요' 다섯 글자가 URL에서 열다섯 개의 %XX로 늘어나는 이유예요.
URL 인코딩이랑 HTML 인코딩은 같은 건가요?
완전히 달라요. URL 인코딩은 %20, %26처럼 퍼센트 기호를 써요. HTML 인코딩은 &, < 같은 형태를 써요. URL 인코딩은 URL에서 문자를 안전하게 전송하기 위한 거고, HTML 인코딩은 HTML 문서에서 특수문자를 안전하게 표시하기 위한 거예요. 용도가 다르고 형식도 달라요.
인코딩을 빠뜨리면 보안 문제가 생기나요?
생길 수 있어요. 사용자가 입력한 값을 인코딩 없이 URL에 그냥 넣으면 공개 리다이렉트 공격, XSS 공격, HTTP 헤더 인젝션 같은 취약점이 생길 수 있어요. 인코딩은 편의 기능이 아니라 보안 요구사항으로 봐야 해요.
쿼리 파라미터 여러 개를 URL에 안전하게 넣는 제일 쉬운 방법은요?
URLSearchParams를 쓰는 게 제일 안전하고 편해요. new URLSearchParams({ q: '서울 맛집', page: '2' }) 이렇게 하면 인코딩을 자동으로 올바르게 처리해줘요. 문자열 연결로 직접 URL 만드는 것보다 훨씬 안전하고, 여러 값을 다룰 때 코드도 깔끔해요.
▶이 글에서 다룬 도구 바로 사용하기
민재
개발자 겸 테크 라이터. 개발 도구와 파일 변환 기술을 깊이 있게 다룹니다.
이 글이 도움이 되셨나요? 새 가이드 알림 받기
스팸 없이, 새 소식만 보내드립니다. 언제든 취소 가능. · 구독 시 개인정보처리방침에 동의합니다.