x-www-form-urlencoded: 완벽 가이드 (2026)
application/x-www-form-urlencoded 콘텐츠 유형에 대해 알아야 할 모든 것
Hypereal로 구축 시작하기
단일 API를 통해 Kling, Flux, Sora, Veo 등에 액세스하세요. 무료 크레딧으로 시작하고 수백만으로 확장하세요.
신용카드 불필요 • 10만 명 이상의 개발자 • 엔터프라이즈 지원
x-www-form-urlencoded: 완벽 가이드 (2026)
application/x-www-form-urlencoded는 HTTP에서 가장 기본적인 콘텐츠 타입 중 하나입니다. HTML 폼(form) 제출의 기본 인코딩 방식이며, API, OAuth 흐름, 결제 처리 등에서 여전히 널리 사용되고 있습니다. 수십 년 된 방식임에도 불구하고 어디에서나 쓰이고 있으며, 이를 제대로 이해하지 못하면 미묘한 버그가 발생할 수 있습니다.
이 가이드는 작동 방식, 사용 시기, 인코딩 규칙 및 주요 언어별 코드 예제를 다룹니다.
x-www-form-urlencoded란 무엇인가요?
application/x-www-form-urlencoded는 폼 데이터를 요청 본문(request body)에 키-값 쌍(key-value pairs)으로 인코딩하는 콘텐츠 타입입니다. 데이터는 URL 쿼리 파라미터와 동일한 형식으로 구성됩니다.
name=John+Doe&email=john%40example.com&age=30
주요 규칙:
- 키-값 쌍은
&로 구분됩니다. - 키와 값은
=로 구분됩니다. - 공백은
+(또는%20)로 인코딩됩니다. - 특수 문자는 퍼센트 인코딩(percent-encoded)됩니다. (예:
@는%40이 됨) - 전체 본문은 단일 평면 문자열(single flat string)입니다. (중첩 구조 없음)
HTTP 요청 예시
POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 47
name=John+Doe&email=john%40example.com&age=30
인코딩 규칙
버그를 피하기 위해서는 인코딩 규칙을 이해하는 것이 중요합니다.
인코딩해야 하는 문자
| 문자 | 인코딩 결과 | 이유 |
|---|---|---|
| 공백 | + 또는 %20 |
구분자 충돌 방지 |
& |
%26 |
쌍 구분자 |
= |
%3D |
키-값 구분자 |
+ |
%2B |
공백으로 사용됨 |
@ |
%40 |
예약된 문자 |
# |
%23 |
프래그먼트 식별자 |
/ |
%2F |
경로 구분자 |
? |
%3F |
쿼리 스트링 마커 |
% |
%25 |
인코딩 접두사 |
! |
%21 |
예약됨 |
' |
%27 |
예약됨 |
( |
%28 |
예약됨 |
) |
%29 |
예약됨 |
인코딩이 필요 없는 문자
알파벳과 숫자(A-Z, a-z, 0-9) 및 비예약 문자(-, _, ., ~)는 인코딩할 필요가 없습니다.
공백 인코딩: + vs %20
이는 자주 혼동되는 부분입니다.
- form-urlencoded 본문에서: 공백은 보통
+로 인코딩됩니다. - URL 경로 및 쿼리 스트링(RFC 3986)에서: 공백은
%20으로 인코딩됩니다.
form-urlencoded 본문에서는 +와 %20 모두 유효하지만, +가 관례입니다. 대부분의 라이브러리는 기본적으로 +를 사용합니다.
Form 본문: name=John+Doe (올바름)
Form 본문: name=John%20Doe (이 또한 올바름)
URL 경로: /users/John%20Doe (올바름)
URL 경로: /users/John+Doe (틀림 - +가 문자 그대로 해석됨)
코드 예제
JavaScript / TypeScript (Fetch API)
// URLSearchParams 사용 (권장)
const params = new URLSearchParams();
params.append("name", "John Doe");
params.append("email", "john@example.com");
params.append("age", "30");
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params.toString(),
// 출력: name=John+Doe&email=john%40example.com&age=30
});
const data = await response.json();
// 축약형: URLSearchParams를 body에 직접 전달
const response = await fetch("https://api.example.com/users", {
method: "POST",
body: new URLSearchParams({
name: "John Doe",
email: "john@example.com",
age: "30",
}),
});
// body가 URLSearchParams이면 Content-Type이 자동으로 설정됩니다.
Python (requests)
import requests
# requests는 data= 파라미터를 사용하면 폼 데이터를 자동으로 인코딩합니다.
response = requests.post(
"https://api.example.com/users",
data={
"name": "John Doe",
"email": "john@example.com",
"age": 30
}
)
print(response.json())
# urllib를 사용한 수동 인코딩
from urllib.parse import urlencode
body = urlencode({
"name": "John Doe",
"email": "john@example.com",
"age": 30
})
print(body)
# 출력: name=John+Doe&email=john%40example.com&age=30
cURL
# -d 플래그 사용 (자동으로 Content-Type 설정)
curl -X POST https://api.example.com/users \
-d "name=John+Doe" \
-d "email=john%40example.com" \
-d "age=30"
# --data-urlencode 사용 (인코딩을 자동으로 처리)
curl -X POST https://api.example.com/users \
--data-urlencode "name=John Doe" \
--data-urlencode "email=john@example.com" \
--data-urlencode "age=30"
Go
package main
import (
"fmt"
"net/http"
"net/url"
"strings"
)
func main() {
data := url.Values{}
data.Set("name", "John Doe")
data.Set("email", "john@example.com")
data.Set("age", "30")
resp, err := http.Post(
"https://api.example.com/users",
"application/x-www-form-urlencoded",
strings.NewReader(data.Encode()),
)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}
PHP
<?php
// cURL 사용
$data = http_build_query([
'name' => 'John Doe',
'email' => 'john@example.com',
'age' => 30
]);
$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
public class FormPost {
public static void main(String[] args) throws Exception {
String body = String.join("&",
"name=" + URLEncoder.encode("John Doe", StandardCharsets.UTF_8),
"email=" + URLEncoder.encode("john@example.com", StandardCharsets.UTF_8),
"age=30"
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}
x-www-form-urlencoded 데이터 파싱
Node.js / Express
import express from "express";
const app = express();
// 폼 데이터 파싱을 위한 내장 미들웨어
app.use(express.urlencoded({ extended: true }));
app.post("/api/users", (req, res) => {
console.log(req.body);
// { name: 'John Doe', email: 'john@example.com', age: '30' }
// 참고: 모든 값은 문자열입니다. 숫자는 수동으로 파싱해야 합니다.
const age = parseInt(req.body.age, 10);
res.json({ received: req.body });
});
Python / Flask
from flask import Flask, request
app = Flask(__name__)
@app.route("/api/users", methods=["POST"])
def create_user():
name = request.form["name"]
email = request.form["email"]
age = int(request.form["age"])
return {"name": name, "email": email, "age": age}
Go (net/http)
func handler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
name := r.FormValue("name")
email := r.FormValue("email")
age := r.FormValue("age")
fmt.Fprintf(w, "Name: %s, Email: %s, Age: %s", name, email, age)
}
x-www-form-urlencoded vs. 다른 콘텐츠 타입
비교 테이블
| 기능 | x-www-form-urlencoded | multipart/form-data | application/json |
|---|---|---|---|
| 파일 업로드 | 불가 | 가능 | Base64 인코딩만 가능 |
| 중첩 데이터 | 불가 (평면 구조) | 불가 (평면 구조) | 가능 (기본 지원) |
| 바이너리 데이터 | 불가 | 가능 | Base64 인코딩만 가능 |
| 가독성(사람) | 보통 | 낮음 | 높음 |
| 인코딩 오버헤드 | 중간 | 파일 전송 시 낮음 | 낮음 |
| 브라우저 네이티브 | 예 (기본 폼) | 예 (enctype 사용) | JavaScript 필요 |
| 배열 지원 | 관례 기반 | 관례 기반 | 네이티브 지원 |
각각의 사용 시기
다음의 경우 x-www-form-urlencoded를 사용하세요:
- 단순한 HTML 폼 제출
- OAuth 2.0 토큰 엔드포인트 (사양에 지정됨)
- 결제 API 제출 (Stripe, PayPal)
- 중첩이 없는 단순 키-값 데이터
- 레거시 API 호환성 유지
다음의 경우 multipart/form-data를 사용하세요:
- 파일 업로드
- 바이너리 데이터 전송
- 폼 필드와 파일을 함께 전송
다음의 경우 application/json을 사용하세요:
- RESTful API 엔드포인트
- 중첩되거나 복잡한 데이터 구조
- 현대적인 프론트엔드-백엔드 통신
- 요청 본문에 배열이나 객체 포함
배열 및 중첩 데이터 처리
x-www-form-urlencoded 데이터는 기본적으로 배열이나 중첩 객체를 지원하지 않지만, 흔히 사용되는 관례가 있습니다.
배열(Arrays)
# 키 반복 (가장 일반적)
colors=red&colors=blue&colors=green
# 브래킷 표기법 (PHP 스타일)
colors[]=red&colors[]=blue&colors[]=green
# 인덱스 표기법
colors[0]=red&colors[1]=blue&colors[2]=green
// JavaScript: 배열 보내기
const params = new URLSearchParams();
params.append("colors", "red");
params.append("colors", "blue");
params.append("colors", "green");
console.log(params.toString());
// 출력: colors=red&colors=blue&colors=green
// 배열 읽기
const colors = params.getAll("colors");
// ["red", "blue", "green"]
중첩 객체(Nested Objects)
# 브래킷 표기법 (PHP/Rails 스타일)
user[name]=John&user[address][city]=NYC&user[address][zip]=10001
모든 서버가 중첩된 브래킷 표기법을 파싱하는 것은 아닙니다. Express에서는 urlencoded 미들웨어에 extended: true 설정이 필요합니다. 만약 중첩 데이터가 필요하다면 JSON 사용을 고려하세요.
흔한 실수와 주의사항
1. 특수 문자 인코딩 누락
// 틀림 - 값 안의 & 기호가 파싱을 깨뜨림
const body = "company=AT&T&city=Dallas";
// 결과: { company: "AT", T: "", city: "Dallas" }
// 올바름 - &를 인코딩함
const body = "company=AT%26T&city=Dallas";
// 결과: { company: "AT&T", city: "Dallas" }
// 가장 좋음 - URLSearchParams 사용
const params = new URLSearchParams({ company: "AT&T", city: "Dallas" });
// 자동으로 인코딩: company=AT%26T&city=Dallas
2. 이중 인코딩(Double Encoding)
// 틀림 - 이미 인코딩된 문자열을 또 인코딩함
const encoded = encodeURIComponent("John+Doe");
// 결과: "John%2BDoe" (+ 기호가 다시 인코딩됨)
// 올바름 - 원본 값을 한 번만 인코딩함
const params = new URLSearchParams({ name: "John Doe" });
// 결과: name=John+Doe
3. 값을 문자열이 아닌 것으로 취급
x-www-form-urlencoded의 모든 값은 문자열입니다. 숫자, 불리언, null 등은 서버 측에서 변환해야 합니다.
// 클라이언트 전송: active=true&count=5
// 서버 수신:
req.body.active // "true" (불리언이 아닌 문자열)
req.body.count // "5" (숫자가 아닌 문자열)
// 항상 명시적으로 파싱하십시오:
const active = req.body.active === "true";
const count = parseInt(req.body.count, 10);
4. Content-Type 헤더 누락
일부 프레임워크는 Content-Type 헤더를 자동으로 설정하지 않습니다.
// 틀림 - Content-Type 누락으로 서버가 본문을 파싱하지 못할 수 있음
fetch("/api/data", {
method: "POST",
body: "name=John&age=30",
});
// 올바름 - 명시적인 Content-Type 설정
fetch("/api/data", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "name=John&age=30",
});
// 역시 올바름 - URLSearchParams가 자동으로 헤더를 설정함
fetch("/api/data", {
method: "POST",
body: new URLSearchParams({ name: "John", age: "30" }),
});
OAuth 2.0과 x-www-form-urlencoded
OAuth 2.0 사양은 토큰 엔드포인트가 x-www-form-urlencoded 데이터를 수락하도록 규정하고 있습니다. 이는 실제 환경에서 가장 흔히 볼 수 있는 사용 사례 중 하나입니다.
// OAuth 2.0 토큰 교환
const response = await fetch("https://oauth.provider.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: "auth_code_from_redirect",
redirect_uri: "https://yourapp.com/callback",
client_id: "your_client_id",
client_secret: "your_client_secret",
}),
});
const tokens = await response.json();
// { access_token: "...", refresh_token: "...", expires_in: 3600 }
결론
application/x-www-form-urlencoded는 HTML 폼, OAuth 흐름 및 많은 API에서 필수적으로 사용되는 단순하고 잘 정립된 인코딩 방식입니다. 기억해야 할 핵심은 인코딩 시 항상 라이브러리 함수를 사용하고(수동으로 문자열을 만들지 마세요), 공백 인코딩의 차이(+ vs %20)를 인지하며, 서버 측에서는 값을 문자열로 파싱해야 한다는 점입니다.
현대적인 API 개발에서는 복잡한 데이터 구조를 위해 보통 JSON을 선호하지만, 단순한 폼이나 OAuth와 같이 사양에서 요구하는 경우에는 x-www-form-urlencoded가 여전히 최선의 선택입니다.
텍스트 프롬프트를 통해 이미지, 비디오, 오디오를 생성하는 등 미디어 생성 API를 구축하고 있다면, Hypereal AI는 JSON 요청/응답 형식을 갖춘 직관적인 REST API를 제공합니다. 여러분의 애플리케이션에 AI 미디어 기능을 추가하려면 확인해 보세요.
