← 모두의 툴

URL 디코딩에서 '+'가 공백이 안 되는 이유 — 이중 디코딩 완전정리

가이드 · 2026-08-19 최종 확인

검색창이나 폼에서 복사한 q=hello+world 같은 쿼리스트링을 URL 디코더에 붙여넣었는데 +가 공백으로 안 바뀌고 그대로 남아있는 경험, 한 번쯤 있을 겁니다. 오류가 아닙니다. JavaScript의 decodeURIComponent·decodeURI는 애초에 +를 다루도록 설계된 함수가 아니기 때문입니다. 왜 그런지, 그리고 이중으로 인코딩된 값을 디코딩할 때 무엇을 조심해야 하는지 정리했습니다. (인코딩 쪽에서 이중 인코딩이 왜 생기는지는 이중 인코딩(%2520) 가이드에서 다뤘으니, 이 글은 디코딩·+ 처리·오류 케이스에 집중합니다.)

1. '+'=공백은 사실 두 규격이 뒤섞인 결과

URL에서 문자를 표현하는 규칙은 사실 하나가 아니라 두 개가 겹쳐 있습니다. RFC 3986이 정의하는 일반 퍼센트 인코딩은 공백을 %20으로 표현합니다. encodeURIComponent·decodeURIComponent·decodeURI가 다루는 대상이 바로 이 규격입니다. 반면 application/x-www-form-urlencoded는 HTML <form method="GET"> 제출 시 브라우저가 쿼리스트링을 만들 때 쓰는 별도 규격으로, 여기서는 공백을 +로 표현합니다. 이 규격은 원래 HTTP 폼 데이터 전송을 위해 만들어진 것이지 일반 URI 문법의 일부가 아닙니다. 그래서 JavaScript의 표준 디코딩 함수들은 이 규격을 모르고, + 문자를 그냥 '더하기 문자 그 자체'로 취급해 손대지 않고 그대로 통과시킵니다.

2. 이 도구가 실제로 하는 일: '+ → 공백' 체크박스

이 사이트의 URL 디코더는 Auto·Component·Full URL 세 가지 모드 모두에서 내부적으로 decodeURIComponentdecodeURI만 사용합니다. 즉 기본 상태에서는 +를 절대 건드리지 않습니다. 폼·검색어 데이터를 다루는 경우를 위해 별도로 + → 공백 체크박스를 제공하는데, 이걸 켜면 디코딩을 실행하기 전에 정규식으로 리터럴 +를 모두 공백으로 먼저 치환한 뒤 나머지 %XX 시퀀스를 디코딩합니다. 순서가 중요합니다 — 이미 %2B로 인코딩되어 있던 진짜 '+' 문자는 아직 %2B 상태이므로 이 치환 단계에서 영향을 받지 않고, 뒤이은 퍼센트 디코딩 단계에서 정상적으로 '+' 문자로 복원됩니다. 반대로 먼저 퍼센트 디코딩부터 하면 %2B가 이미 '+'로 풀려버려 원래 있던 '+'와 공백이었던 '+'를 구분할 수 없게 됩니다.

입력모드결과설명
a+b%20c기본(체크박스 끔)a+b c%20만 공백으로, 리터럴 +는 그대로 남음
a+b%20c+ → 공백 켬a b c+를 먼저 공백 치환 후 %20도 공백으로 디코딩
hello%2Bworld+ → 공백 켬hello+world%2B는 치환 대상이 아니어서 디코딩 후 '+'로 정상 복원

3. 이중 디코딩: 왜 한 번으로는 안 풀릴 때가 있나

어딘가에서 값이 실수로 두 번 인코딩됐다면(예: %2520), 이 디코더에서 한 번만 디코딩을 돌리면 %2520%20까지만 풀리고 공백까지는 도달하지 못합니다. 결과창에 %20이나 %25로 시작하는 시퀀스가 여전히 남아 있다면 디코딩이 한 단계 더 필요하다는 신호입니다. 이 도구는 자동으로 반복 디코딩을 하지 않으므로, 결과를 다시 입력창에 붙여넣고 Decode를 한 번 더 눌러야 합니다. 단, 원문 자체에 우연히 %25로 시작하는 값이 들어있는 경우(예: 할인율 "50%"를 인코딩한 50%25)도 있으므로, 반복 디코딩 전에 결과가 실제로 의미 있는 텍스트인지부터 눈으로 확인하는 게 안전합니다.

4. URIError가 나는 진짜 이유

decodeURIComponent% 뒤에 정확히 16진수 두 자리가 와야만 유효한 시퀀스로 인정합니다. %GG처럼 16진수가 아닌 문자가 오거나 %2처럼 문자열이 중간에 끊기면 즉시 URIError: URI malformed를 던집니다. 한글·중국어·이모지처럼 UTF-8에서 여러 바이트로 표현되는 문자는 %EC%95%88처럼 바이트 여러 개가 한 세트로 묶여 있어야 하는데, 이 세트 중 일부만 잘려서 들어오면(복사 과정에서 문자열이 중간에 끊긴 경우 등) 바이트 조합이 깨져 같은 오류가 발생합니다. 이 도구는 Auto 모드에서 decodeURIComponent가 실패하면 자동으로 decodeURI를 대체 시도하지만, 두 함수 모두 같은 이유로 실패하는 근본적으로 깨진 입력이라면 결국 오류 메시지가 표시됩니다.

5. 정리

자주 묻는 질문

왜 decodeURIComponent는 '+'를 공백으로 안 바꾸나요?

decodeURIComponent는 RFC 3986 퍼센트 인코딩(%XX)만 처리하도록 설계된 함수라, '+' 기호는 애초에 이 함수가 다루는 대상이 아닙니다. '+'=공백 규칙은 별개의 규격인 application/x-www-form-urlencoded(HTML 폼 GET 제출)에서 나온 관례입니다.

URL 디코더의 '+ → 공백' 체크박스는 언제 켜야 하나요?

검색창이나 HTML 폼에서 복사한 쿼리스트링(예: q=hello+world)을 디코딩할 때 켜세요. REST API에서 encodeURIComponent로 만든 값에는 보통 '+'가 나오지 않으므로 끈 상태로 두는 것이 안전합니다.

이중 인코딩된 값을 한 번만 디코딩하면 어떻게 되나요?

%2520처럼 두 번 인코딩된 값을 한 번만 디코딩하면 공백이 아니라 %20이라는 문자 그대로의 문자열이 남습니다. 완전히 되돌리려면 디코더를 두 번 실행해야 합니다.

decodeURIComponent가 URIError를 던지는 경우는?

%GG처럼 %뒤에 유효한 16진수 두 자리가 오지 않거나, %2처럼 시퀀스가 중간에 끊긴 경우 URIError가 발생합니다. UTF-8 바이트 조합이 깨진 경우(잘못 잘린 멀티바이트 시퀀스)도 같은 오류를 냅니다.