HTTP GET과 POST 차이
웹 개발을 시작하면 GET과 POST를 아주 자주 만난다.
목록을 불러올 때는 GET, 회원가입을 할 때는 POST를 쓰는 식이다.
처음에는 “데이터를 URL에 담으면 GET, body에 담으면 POST” 정도로 이해하기 쉽다.
이 설명도 출발점으로는 괜찮다.
하지만 실제 기준은 조금 더 넓다.
핵심은 요청의 의도다.GET은 리소스를 조회하기 위한 메서드다.POST는 서버가 요청 본문을 처리해서 어떤 동작을 수행하게 하는 메서드다.
HTTP method의 역할
HTTP 요청에는 method가 있다.
method는 서버에게 이 요청을 어떤 의도로 처리하면 되는지 알려주는 값이다.
GET /posts HTTP/1.1
Host: example.comPOST /posts HTTP/1.1
Host: example.com
Content-Type: application/json
{
"title": "hello"
}두 요청은 같은 /posts 경로를 사용할 수도 있다.
하지만 method가 다르므로 의미가 다르다.
GET /posts는 보통 글 목록을 조회한다.POST /posts는 보통 새 글을 생성한다.
URL만 보고 판단하는 것이 아니라 method와 URL을 함께 봐야 한다.
GET의 기준
GET은 지정한 리소스의 표현을 요청하는 메서드다.
쉽게 말하면 서버에서 데이터를 가져올 때 쓴다.
GET /products HTTP/1.1
Host: example.com검색 조건이나 페이지 번호처럼 조회 조건이 필요하면 query string을 붙인다.
GET /products?keyword=keyboard&page=2 HTTP/1.1
Host: example.com브라우저 주소창에서 직접 열 수 있는 URL도 대부분 GET 요청이다.
링크를 클릭하거나 이미지를 불러오는 일도 보통 GET으로 처리된다.
그래서 GET 요청은 URL에 요청 정보가 드러난다.
검색어, 페이지 번호, 정렬 기준처럼 공유해도 되는 조회 조건에 잘 맞는다.
/products?keyword=keyboard&sort=price반대로 비밀번호나 토큰처럼 노출되면 안 되는 값은 query string에 넣지 않는 편이 좋다.
URL은 브라우저 기록, 서버 로그, 분석 도구, 공유된 링크에 남을 수 있기 때문이다.
POST의 기준
POST는 데이터를 서버로 보내고, 서버가 그 데이터를 처리하게 할 때 쓴다.
대표적으로 회원가입, 로그인, 게시글 작성, 주문 생성, 파일 업로드 같은 작업이다.
POST /signup HTTP/1.1
Host: example.com
Content-Type: application/json
{
"email": "user@example.com",
"password": "secret"
}POST의 데이터는 보통 request body에 담긴다.
body의 형식은 Content-Type 헤더로 알려준다.
Content-Type: application/jsonHTML form에서는 application/x-www-form-urlencoded나 multipart/form-data도 자주 사용한다.
파일 업로드를 다룰 때는 보통 multipart/form-data가 쓰인다.
<form method="post" action="/profile" enctype="multipart/form-data">
<input type="file" name="avatar" />
<button type="submit">저장</button>
</form>POST는 서버 상태를 바꾸는 작업에 자주 사용된다.
새 리소스를 만들거나, 처리 작업을 시작하거나, 폼 제출 결과를 서버에 반영하는 식이다.
query string과 body
GET은 보통 query string으로 조건을 표현한다.
GET /search?q=css&page=1 HTTP/1.1
Host: example.comPOST는 보통 body에 데이터를 담는다.
POST /search HTTP/1.1
Host: example.com
Content-Type: application/json
{
"q": "css",
"page": 1
}둘 중 무엇을 쓸지는 데이터가 어디에 담기는지만으로 결정하지 않는다.
검색처럼 조회 조건을 담는 요청이라면 GET이 자연스럽다.
링크 공유, 새로고침, 북마크, 캐시와 잘 맞기 때문이다.
반대로 주문 생성처럼 서버 상태를 바꾸는 요청이라면 POST가 자연스럽다.
같은 요청이 다시 전송됐을 때 같은 주문이 중복 생성될 수 있기 때문이다.
즉, query string은 조회 조건에 잘 맞고 body는 처리할 데이터에 잘 맞는다.
하지만 더 중요한 기준은 요청이 읽기인지 변경인지다.
safe의 의미
HTTP에서 safe method는 클라이언트가 서버 상태 변경을 의도하지 않는 메서드다.GET은 safe method에 속한다.
여기서 safe는 “절대 아무 일도 일어나지 않는다”는 뜻이 아니다.
서버가 접근 로그를 남기거나 통계를 기록할 수는 있다.
중요한 것은 클라이언트가 리소스 변경을 요청하지 않는다는 점이다.
그래서 아래처럼 삭제를 GET으로 만들면 의미가 어긋난다.
GET /posts/1/delete HTTP/1.1
Host: example.com링크 미리보기, 검색 엔진 크롤러, 브라우저 prefetch 같은 동작이 GET을 자동으로 호출할 수 있다.GET 요청이 삭제나 결제를 수행한다면 의도하지 않은 변경이 생길 수 있다.
삭제는 보통 DELETE, 생성은 보통 POST, 수정은 보통 PUT이나 PATCH 같은 메서드를 고려한다.
이 글에서는 GET과 POST를 중심으로 보지만, safe의 기준은 API 설계에서 꽤 중요하다.
idempotent의 의미
idempotent는 같은 요청을 여러 번 보내도 서버에 의도한 효과가 한 번 보낸 것과 같다는 뜻이다.GET은 safe method이므로 idempotent로 본다.
GET /posts/1 HTTP/1.1
Host: example.com같은 글을 여러 번 조회한다고 해서 글이 여러 개 생기지는 않는다.
응답 내용은 시간이 지나며 달라질 수 있지만, 조회 요청 자체가 서버 상태 변경을 의도하지 않는다.
POST는 일반적으로 idempotent가 보장되지 않는다.
POST /orders HTTP/1.1
Host: example.com
Content-Type: application/json
{
"productId": 10,
"quantity": 1
}이 주문 생성 요청을 여러 번 보내면 주문이 여러 개 만들어질 수 있다.
그래서 결제나 주문 같은 작업에서는 중복 제출 방지, idempotency key, 서버 측 중복 처리 같은 장치가 필요할 수 있다.
이 차이는 네트워크 재시도에서도 중요하다.GET은 실패했을 때 다시 시도하기 비교적 쉽지만, POST는 같은 작업이 중복 실행될 수 있는지 생각해야 한다.
캐시와 북마크
GET은 캐시와 잘 맞는다.
같은 URL이 같은 리소스 표현을 가리킨다면 브라우저나 CDN이 응답을 재사용할 수 있다.
GET /posts/1 HTTP/1.1
Host: example.comURL에 조회 조건이 드러나므로 링크 공유와 북마크도 쉽다.
https://example.com/search?q=javascript&page=2검색 결과 페이지나 상품 목록 페이지에는 이 특성이 유용하다.
사용자가 새로고침을 해도 같은 조건으로 다시 조회할 수 있고, 링크를 다른 사람에게 보내기도 쉽다.
POST 응답도 조건에 따라 캐시될 수는 있지만, 일반적인 웹 개발에서는 GET보다 캐시 대상으로 다루기 어렵다.
요청 본문이 URL에 드러나지 않고, 요청 자체가 서버 작업을 의미하는 경우가 많기 때문이다.
조회 화면을 만들 때는 먼저 GET으로 표현할 수 있는지 본다.
그 편이 브라우저와 HTTP의 기본 동작에 잘 맞는다.
fetch 사용 예
브라우저에서 fetch를 사용할 때 GET은 기본 method다.
const response = await fetch('/api/posts?tag=css')
const posts = await response.json()명시적으로 적어도 된다.
const response = await fetch('/api/posts?tag=css', {
method: 'GET',
})POST는 method와 body, Content-Type을 함께 지정하는 경우가 많다.
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'GET과 POST 차이',
}),
})응답 처리와 HTTP 에러 처리는 method와 별개로 챙겨야 한다.fetch는 404나 500 응답을 받았다고 해서 자동으로 catch로 보내지 않는다.
이 부분이 헷갈리면 fetch와 axios 차이를 같이 보면 좋다.
form 사용 예
HTML form은 method 속성으로 GET이나 POST를 지정할 수 있다.
검색 폼은 GET이 잘 맞는다.
<form method="get" action="/search">
<input name="q" />
<button type="submit">검색</button>
</form>이 폼을 제출하면 브라우저는 입력값을 query string으로 만든다.
/search?q=css회원가입 폼은 POST가 잘 맞는다.
<form method="post" action="/signup">
<input name="email" type="email" />
<input name="password" type="password" />
<button type="submit">가입</button>
</form>이런 값은 URL에 남기기보다 request body로 보내는 편이 낫다.
다만 POST를 쓴다고 해서 자동으로 안전해지는 것은 아니다.
민감한 값을 다루려면 HTTPS, 서버 검증, 쿠키 설정, CSRF 방어 같은 보안 기준도 함께 봐야 한다.
POST와 보안의 오해
POST는 데이터가 URL에 보이지 않는다는 장점이 있다.
하지만 그것만으로 보안이 해결되지는 않는다.
브라우저 개발자 도구의 Network 탭에서는 POST body도 확인할 수 있다.
네트워크 구간이 암호화되지 않았다면 요청 내용이 노출될 수 있다.
민감한 데이터를 보낼 때 중요한 기준은 GET이냐 POST냐보다 HTTPS 사용 여부와 서버의 처리 방식이다.
비밀번호를 query string에 넣지 않는 것은 기본이고, 전송 자체도 HTTPS로 보호해야 한다.
또 로그인이나 결제처럼 상태가 바뀌는 요청은 CSRF 같은 공격도 고려해야 한다.
method 선택은 첫 단추일 뿐이고, 보안 설계 전체를 대신하지 않는다.
선택 기준
데이터를 조회하고 URL로 공유할 수 있어야 한다면 GET을 쓴다.
검색, 목록 조회, 상세 조회, 필터링, 페이지 이동이 여기에 가깝다.
서버 상태를 만들거나 바꾸는 작업이라면 POST를 쓴다.
회원가입, 로그인, 글 작성, 댓글 등록, 주문 생성, 파일 업로드가 여기에 가깝다.
같은 요청이 여러 번 전송될 수 있는 상황도 생각한다.GET은 재시도와 캐시에 잘 맞는다.POST는 중복 실행이 문제가 될 수 있으므로 서버에서 중복 처리를 고려해야 한다.
정리하면 기준은 단순하다.
읽기는 GET이다.
처리와 생성은 POST다.
URL에 남아도 되는 조회 조건은 query string에 두고, 서버가 처리해야 하는 데이터는 body에 둔다.
참고 자료













