WeniVooks

검색

웹/네트워크/HTTP 베이스캠프

Request

우리 수업에서는 실습하진 않지만 아래와 같은 툴로 브라우저를 통하지 않고 직접 왔다 갔다 하는 Request와 Response를 전부 볼 수 있습니다.

(부록) 와이어샤크(Wireshark)

메시지 구조

GET /index.html HTTP/1.1              # 요청라인
user-agent: MSIE 6.0; Windows NT 5.0  # 헤더
accept: text/html; */*
cookie: name = value
referer: http://www.naver.com
host: www.paullab.co.kr
GET /index.html HTTP/1.1              # 요청라인
user-agent: MSIE 6.0; Windows NT 5.0  # 헤더
accept: text/html; */*
cookie: name = value
referer: http://www.naver.com
host: www.paullab.co.kr
HTTP/1.1 200 OK                       # 응답라인 
Server: Apache/2.4.41 (Ubuntu)        # 헤더 
Content-Type: text/html; charset=UTF-8 
Content-Length: 1024 
Date: Mon, 01 Apr 2024 10:30:00 GMT 
                                      # 빈줄 
<!DOCTYPE html>                       # 본문
<html>
  <head>
    <title>Example Page</title>
  </head>
  <body>
    <h1>Welcome to Example Page</h1>
    <p>This is an example HTTP response.</p>
  </body>
</html>
 
HTTP/1.1 200 OK                       # 응답라인 
Server: Apache/2.4.41 (Ubuntu)        # 헤더 
Content-Type: text/html; charset=UTF-8 
Content-Length: 1024 
Date: Mon, 01 Apr 2024 10:30:00 GMT 
                                      # 빈줄 
<!DOCTYPE html>                       # 본문
<html>
  <head>
    <title>Example Page</title>
  </head>
  <body>
    <h1>Welcome to Example Page</h1>
    <p>This is an example HTTP response.</p>
  </body>
</html>
 

실제 요청과 응답

응답 코드도 가져올 수 있습니다.

6.1 Request

GET /index.html HTTP/1.1
user-agent: MSIE 6.0; Windows NT 5.0
accept: text/html; */*
cookie: name = value
referer: http://www.naver.com
host: www.paullab.co.kr
GET /index.html HTTP/1.1
user-agent: MSIE 6.0; Windows NT 5.0
accept: text/html; */*
cookie: name = value
referer: http://www.naver.com
host: www.paullab.co.kr
  1. 요청 라인

    1. 데이터 처리 방식(요청 메서드)
    2. 요청 페이지
    3. 프로토콜 버전
  2. Header

    1. User-Agent: 사용자 웹 브라우저 종류 및 버전 정보.

    2. Accept: 웹 서버로부터 수신되는 데이터 중 웹 브라우저가 처리할 수 있는 데이터 타입을 의미.

      여기서 text/html은 text, html 형태의 문서를 처리할 수 있고,  */*는 모든 문서를 처리할 수 있다는 의미. (이를 MIME 타입이라 부르기도 한다.)

    3. Cookie: HTTP 프로토콜 자체가 세션을 유지하지 않는 State-less(접속상태를 유지하지 않는) 방식이기 때문에 로그인 인증을 위한 사용자 정보를 기억하려고 만든 인위적인 값. 즉 사용자가 정상적인 로그인 인증 정보를 가지고 있다는 것을 판단하고자 사용.

      접속 상태에 대한 내용을 후반부에 다루게 됩니다.

    4. Referer: 현재 페이지 접속 전에 어느 사이트를 경유했는지 알려주는 도메인 혹은 URL 정보.

    5. Host: 사용자가 요청한 도메인 정보.

request header 부분은 통신에서 필요한 경우 아래 fetch의 옵션으로 넣어야 하는 값입니다.

fetch(`${URL}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data),
});
fetch(`${URL}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data),
});
6.1.1 HTTP 요청 메서드
HTTP Method Description
GET 주로 리소스 취득 (URL 뒤에 이어붙이는 방식 - 작은 값들)
POST 주로 리소스 생성 (Body에 붙이는 방식 - 상대적으로 큰 용량)
PUT 리소스의 모든 것을 업데이트
DELETE 리소스 삭제
PATCH 리소스의 일부를 업데이트
HEAD HTTP 헤더 정보만 요청, 해당 자원의 존재 여부 확인 목적.
OPTIONS 웹서버가 지원하는 메소드 종류 반환 요청
TRACE 요청 리소스가 수신되는 경로 확인
CONNECT 프록시 서버로의 터널링을 통해 양방향 통신을 시작
6.1.2 HTTP 요청 메서드 실습

실습을 위해 미리 준비된 서버를 사용하겠습니다. 이러한 서버를 모의(Mock) 서버라고 합니다. 모의 서버는 실제 서버와 동일한 API를 제공하지만, 데이터베이스 대신 가짜 데이터를 사용합니다. 이를 통해 프론트엔드 개발자는 백엔드 개발이 완료되기 전에도 API와의 통신을 테스트할 수 있습니다. 다양한 라이브러리와 프레임워크에서 모의 서버를 제공하며, 필요에 따라 직접 설정할 수도 있습니다.

(부록) json-server로 Mock 서버 세팅
6.1.2.1 GET: 서버! 네가 가진 정보를 줘!

GET 메서드는 서버에 특정 리소스를 요청할 때 사용됩니다. 요청 패킷의 구조는 다음과 같습니다.

GET /753/product HTTP/1.1
Host: https://eduapi.weniv.co.kr/
GET /753/product HTTP/1.1
Host: https://eduapi.weniv.co.kr/
  • GET: HTTP 요청 메서드를 나타냅니다. 이 예시에서는 GET 메서드를 사용하여 리소스를 요청합니다.
  • /753/product: 요청하려는 리소스의 경로를 나타냅니다. 이 경로는 서버에서 제공하는 API의 엔드포인트에 해당합니다. 여기서 753번은 다양한 사람들이 실습을 할 수 있도록 저희쪽에서 세팅해놓은 임의의 값입니다. 1 ~ 1000번까지 사용이 가능합니다.
  • HTTP/1.1: 사용 중인 HTTP 버전을 나타냅니다.
  • Host: https://eduapi.weniv.co.kr/: 요청을 보내는 서버의 호스트 이름과 포트 번호를 나타냅니다.

JavaScript의 Fetch API를 사용하여 GET 요청을 보내는 예시 코드는 다음과 같습니다.

fetch('https://eduapi.weniv.co.kr/753/product', {
  method: 'GET',
})
  .then((response) => response.json())
  .then((data) => {
    console.log('성공:', data);
  })
  .catch((error) => {
    console.error('실패:', error);
  });
fetch('https://eduapi.weniv.co.kr/753/product', {
  method: 'GET',
})
  .then((response) => response.json())
  .then((data) => {
    console.log('성공:', data);
  })
  .catch((error) => {
    console.error('실패:', error);
  });

위 코드는 method: 'GET' 을 적지 않은 아래 요청과 동일합니다.

fetch('https://eduapi.weniv.co.kr/753/product')
  .then(response => response.json())
  .then(data => {
    console.log('성공:', data);
  })
  .catch(error => {
    console.error('실패:', error);
  });
fetch('https://eduapi.weniv.co.kr/753/product')
  .then(response => response.json())
  .then(data => {
    console.log('성공:', data);
  })
  .catch(error => {
    console.error('실패:', error);
  });
6.1.2.2 POST: 서버! 새로운 상품 정보를 줄게! 데이터를 생성해줘!

POST 메서드는 서버에 새로운 리소스를 생성할 때 사용됩니다. 요청 패킷의 구조는 다음과 같습니다.

POST /753/product HTTP/1.1
Host: https://eduapi.weniv.co.kr/
Content-Type: application/json
Content-Length: 1560

{
    "id": 8,
    "productName": "hello world keyring",
    "price": 12500,
    ... 생략 ...
}
POST /753/product HTTP/1.1
Host: https://eduapi.weniv.co.kr/
Content-Type: application/json
Content-Length: 1560

{
    "id": 8,
    "productName": "hello world keyring",
    "price": 12500,
    ... 생략 ...
}
  • POST: HTTP 요청 메서드를 나타냅니다. 이 예시에서는 POST 메서드를 사용하여 새로운 리소스를 생성합니다.
  • /753/product: 생성하려는 리소스의 경로를 나타냅니다. 이 경로는 서버에서 제공하는 API의 엔드포인트에 해당합니다.
  • HTTP/1.1: 사용 중인 HTTP 버전을 나타냅니다.
  • Host: https://eduapi.weniv.co.kr/: 요청을 보내는 서버의 호스트 이름과 포트 번호를 나타냅니다.
  • Content-Type: application/json: 요청 본문의 타입을 나타냅니다. 이 예시에서는 JSON 형식의 데이터를 전송합니다.
  • Content-Length: 1560: 요청 본문의 길이를 나타냅니다. 이 값은 실제 전송되는 데이터의 바이트 수와 일치해야 합니다.
const data = {
  id: 8,
  productName: 'hello world keyring',
  price: 12500,
  stockCount: 100,
  thumbnailImg: 'asset/products/img/1/thumbnailImg.jpg',
  option: [],
  discountRate: 0,
  shippingFee: 1500,
  detailInfoImage: [
    'asset/products/detail/2/detail1.png',
    'asset/products/detail/2/detail2.png',
  ],
  viewCount: 0,
  pubDate: '2222-02-28',
  modDate: '2222-02-28',
};
 
fetch('https://eduapi.weniv.co.kr/753/product', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data),
})
  .then((response) => response.json())
  .then((data) => {
    console.log('성공:', data);
  })
  .catch((error) => {
    console.error('실패:', error);
  });
const data = {
  id: 8,
  productName: 'hello world keyring',
  price: 12500,
  stockCount: 100,
  thumbnailImg: 'asset/products/img/1/thumbnailImg.jpg',
  option: [],
  discountRate: 0,
  shippingFee: 1500,
  detailInfoImage: [
    'asset/products/detail/2/detail1.png',
    'asset/products/detail/2/detail2.png',
  ],
  viewCount: 0,
  pubDate: '2222-02-28',
  modDate: '2222-02-28',
};
 
fetch('https://eduapi.weniv.co.kr/753/product', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data),
})
  .then((response) => response.json())
  .then((data) => {
    console.log('성공:', data);
  })
  .catch((error) => {
    console.error('실패:', error);
  });

실제 데이터가 생성이 되었는지 다시 GET 요청을 통해 확인해보세요.

fetch('https://eduapi.weniv.co.kr/753/product', {
  method: 'GET',
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error(error));
fetch('https://eduapi.weniv.co.kr/753/product', {
  method: 'GET',
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error(error));
6.1.2.3 PUT: 서버야 유저 정보를 덮어씌워 줘!

PUT 메서드는 서버에 이미 존재하는 리소스를 수정할 때 사용됩니다. 인증 값이 꼭 필요한 것은 아니지만 특히 POST, PUT, DELETE는 인증 정보를 요구하는 경우가 많습니다. 이 경우 아래와 같이 인증 값까지 함께 들어가 있게 됩니다. 여기서는 인증을 주석처리하고 갑니다. 실무에서 자주 사용하는 코드이니 인증 값을 넣는 것까지 기억해주세요.

PUT /753/product HTTP/1.1
Host: https://eduapi.weniv.co.kr/
Content-Type: application/json
Content-Length: 1559
Authorization: 토큰값

{
    "id": 1,
    "productName": "test put data",
    "price": 999999,
    ... 생략 ...
}
PUT /753/product HTTP/1.1
Host: https://eduapi.weniv.co.kr/
Content-Type: application/json
Content-Length: 1559
Authorization: 토큰값

{
    "id": 1,
    "productName": "test put data",
    "price": 999999,
    ... 생략 ...
}
const data = {
  id: 1,
  productName: 'test put data',
  price: 999999,
  stockCount: 100,
  thumbnailImg: 'asset/products/img/1/thumbnailImg.jpg',
  option: [],
  discountRate: 0,
  shippingFee: 1500,
  detailInfoImage: [
    'asset/products/detail/2/detail1.png',
    'asset/products/detail/2/detail2.png',
  ],
  viewCount: 0,
  pubDate: '2022-02-28',
  modDate: '2022-02-28',
};
 
// const token = "your_bearer_token_here";
 
fetch('https://eduapi.weniv.co.kr/753/product/1', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    // "Authorization": `Bearer ${token}`,
  },
  body: JSON.stringify(data),
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error(error));
const data = {
  id: 1,
  productName: 'test put data',
  price: 999999,
  stockCount: 100,
  thumbnailImg: 'asset/products/img/1/thumbnailImg.jpg',
  option: [],
  discountRate: 0,
  shippingFee: 1500,
  detailInfoImage: [
    'asset/products/detail/2/detail1.png',
    'asset/products/detail/2/detail2.png',
  ],
  viewCount: 0,
  pubDate: '2022-02-28',
  modDate: '2022-02-28',
};
 
// const token = "your_bearer_token_here";
 
fetch('https://eduapi.weniv.co.kr/753/product/1', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    // "Authorization": `Bearer ${token}`,
  },
  body: JSON.stringify(data),
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error(error));
6.1.2.4 DELETE: 서버야 있던 정보를 삭제해줘!
DELETE /753/product/1 HTTP/1.1
Host: https://eduapi.weniv.co.kr/
Authorization: 토큰값
DELETE /753/product/1 HTTP/1.1
Host: https://eduapi.weniv.co.kr/
Authorization: 토큰값
const productId = 1;
// const token = "your_bearer_token_here";
 
fetch(`https://eduapi.weniv.co.kr/753/product/${productId}`, {
  method: 'DELETE',
  // headers: {
  //     "Authorization": `Bearer ${token}`,
  // },
});
const productId = 1;
// const token = "your_bearer_token_here";
 
fetch(`https://eduapi.weniv.co.kr/753/product/${productId}`, {
  method: 'DELETE',
  // headers: {
  //     "Authorization": `Bearer ${token}`,
  // },
});
6.1.2.5 들어오는 데이터의 원본 보기

실제 서버에서 들어오는 데이터를 확인하고 싶을 때에는 아래와 같이 코드를 작성하면 됩니다.

import http.server
import socketserver
import json
 
class RequestHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        raw_data = self.get_raw_request_data()
        print("Raw GET request data:")
        print(raw_data)
        # 실제로는 아래와 같이 파싱된 데이터를 사용하게 됩니다.
        # parsed_url = urlparse(self.path)
        # query_params = parse_qs(parsed_url.query)
        # response_data = {
        #     'method': 'GET',
        #     'path': self.path,
        #     'headers': dict(self.headers),
        #     'query_params': query_params,
        #     'raw_data': f"GET {self.path} {self.protocol_version}\n{self.headers}"
        # }
 
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps({'message': 'Success'}).encode('utf-8'))
 
    def do_POST(self):
        raw_data = self.get_raw_request_data()
        print("Raw POST request data:")
        print(raw_data)
        
        # 실제로는 아래와 같이 파싱된 데이터를 사용하게 됩니다.
        # content_length = int(self.headers['Content-Length'])
        # post_data = self.rfile.read(content_length).decode('utf-8')
 
        # response_data = {
        #     'method': 'POST',
        #     'path': self.path,
        #     'headers': dict(self.headers),
        #     'post_data': post_data,
        #     'raw_data': f"POST {self.path} {self.protocol_version}\n{self.headers}\n\n{post_data}"
        # }
 
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps({'message': 'Success'}).encode('utf-8'))
 
    def get_raw_request_data(self):
        raw_data = f"{self.command} {self.path} {self.request_version}\n"
        raw_data += str(self.headers)
 
        content_length = self.headers.get('Content-Length')
        if content_length:
            body = self.rfile.read(int(content_length)).decode('utf-8')
            raw_data += f"\n\n{body}"
 
        return raw_data
 
PORT = 8000
 
with socketserver.TCPServer(("", PORT), RequestHandler) as httpd:
    print(f"Serving at port {PORT}")
    httpd.serve_forever()
import http.server
import socketserver
import json
 
class RequestHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        raw_data = self.get_raw_request_data()
        print("Raw GET request data:")
        print(raw_data)
        # 실제로는 아래와 같이 파싱된 데이터를 사용하게 됩니다.
        # parsed_url = urlparse(self.path)
        # query_params = parse_qs(parsed_url.query)
        # response_data = {
        #     'method': 'GET',
        #     'path': self.path,
        #     'headers': dict(self.headers),
        #     'query_params': query_params,
        #     'raw_data': f"GET {self.path} {self.protocol_version}\n{self.headers}"
        # }
 
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps({'message': 'Success'}).encode('utf-8'))
 
    def do_POST(self):
        raw_data = self.get_raw_request_data()
        print("Raw POST request data:")
        print(raw_data)
        
        # 실제로는 아래와 같이 파싱된 데이터를 사용하게 됩니다.
        # content_length = int(self.headers['Content-Length'])
        # post_data = self.rfile.read(content_length).decode('utf-8')
 
        # response_data = {
        #     'method': 'POST',
        #     'path': self.path,
        #     'headers': dict(self.headers),
        #     'post_data': post_data,
        #     'raw_data': f"POST {self.path} {self.protocol_version}\n{self.headers}\n\n{post_data}"
        # }
 
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps({'message': 'Success'}).encode('utf-8'))
 
    def get_raw_request_data(self):
        raw_data = f"{self.command} {self.path} {self.request_version}\n"
        raw_data += str(self.headers)
 
        content_length = self.headers.get('Content-Length')
        if content_length:
            body = self.rfile.read(int(content_length)).decode('utf-8')
            raw_data += f"\n\n{body}"
 
        return raw_data
 
PORT = 8000
 
with socketserver.TCPServer(("", PORT), RequestHandler) as httpd:
    print(f"Serving at port {PORT}")
    httpd.serve_forever()

이 코드를 실행하고 브라우저에서 http://127.0.0.1:8000에 접속하면, 브라우저에서 요청한 데이터를 확인할 수 있습니다. 이번에는 form을 만들어서 데이터를 보내보겠습니다. 두 개의 코드 모두 POST 방식이지만 하나는 form에서 데이터를 직접 전송한 것이고 하나는 JavaScript를 통해 전송한 것입니다.

<form action="http://127.0.0.1:8000" method="post">
  <input type="text" name="username" value="hellopost" />
  <input type="password" name="password" value="worldpost" />
  <input type="submit" value="Login" />
</form>
<form action="http://127.0.0.1:8000" method="post">
  <input type="text" name="username" value="hellopost" />
  <input type="password" name="password" value="worldpost" />
  <input type="submit" value="Login" />
</form>
<form action="http://127.0.0.1:8000" method="post">
  <input type="text" name="username" value="hellopost" />
  <input type="password" name="password" value="worldpost" />
  <button type="button" onclick="sendData()">전송</button>
</form>
 
<script>
  function sendData() {
    // form 요소를 가져옴
    const form = document.getElementById('myForm');
    const formData = new FormData(form);
 
    // 직접 통신을 하지 않게 막음
    event.preventDefault();
 
    // fetch를 통해 데이터 전송
    fetch('http://127.0.0.1:8000', {
      method: 'POST',
      body: formData
    })
    .then(response => response.text())
    .then(data => {
      console.log('응답:', data);
    })
    .catch(error => {
      console.error('에러:', error);
    });
  }
</script>
<form action="http://127.0.0.1:8000" method="post">
  <input type="text" name="username" value="hellopost" />
  <input type="password" name="password" value="worldpost" />
  <button type="button" onclick="sendData()">전송</button>
</form>
 
<script>
  function sendData() {
    // form 요소를 가져옴
    const form = document.getElementById('myForm');
    const formData = new FormData(form);
 
    // 직접 통신을 하지 않게 막음
    event.preventDefault();
 
    // fetch를 통해 데이터 전송
    fetch('http://127.0.0.1:8000', {
      method: 'POST',
      body: formData
    })
    .then(response => response.text())
    .then(data => {
      console.log('응답:', data);
    })
    .catch(error => {
      console.error('에러:', error);
    });
  }
</script>

💡 form 태그에서 지원하는 메서드가 GET과 POST로 제한되는 이유

HTML form 태그는 HTTP 프로토콜의 초기 버전부터 존재해왔기 때문에, 당시에는 GET과 POST 메서드만 지원했습니다. 이는 웹의 초창기에는 단순한 폼 데이터 전송만으로도 충분했기 때문입니다.

반면에 JavaScript는 AJAX(Asynchronous JavaScript and XML)의 등장 이후로 더 다양한 HTTP 메서드를 사용할 수 있게 되었습니다. JavaScript의 XMLHttpRequest 객체나 fetch API를 통해 PUT, DELETE, PATCH 등의 메서드로 요청을 보낼 수 있습니다.

form 태그에 GET과 POST 이외의 메서드를 지원하지 않는 이유는 다음과 같습니다.

  1. 역사적 이유: form 태그는 웹 초창기부터 사용되었기 때문에, 당시의 HTTP 프로토콜에 맞추어 설계되었습니다.
  2. 브라우저 호환성: 모든 브라우저가 form 태그에서 다른 메서드를 지원하는 것은 아닙니다. 호환성을 유지하기 위해 GET과 POST만 사용됩니다.
  3. form의 태생: form은 기본적으로 정보를 '제공'하는 역할을 합니다. 따라서 정보를 제공하는 form에서 delete를 하는 것이 form의 목적에 맞지 않습니다.

form 태그에서는 GET과 POST만 사용할 수 있지만, JavaScript를 사용하면 더 다양한 HTTP 메서드로 요청을 보낼 수 있습니다.

6장 HTTP 메시지 구조6.2 Response