서버로 이미지 보내기에 숨겨진 개념들

👾·2022년 6월 27일
12

HTTP

목록 보기
1/4
post-thumbnail

여행 아카이빙 플랫폼 프로젝트를 개발하던 중, 이미지와 JSON을 동시에 서버로 전송해야하는 부분이 있었다(유저가 여행사진과 여정 텍스트등을 기록하고 저장하는 기능이다). 이전 프로젝트에서는 거의 텍스트만을 가지고 서버와 통신했기 때문에 이미지에 대해서는 생각해 보지 못했고, 당연하게 똑같이 통신하는 코드를 짰으나 바로 실패했다.🥺

한번 막힌 후 "React 이미지 전송"이라고 검색하여 여러 글을 찾아보니 모두 FormData를 사용하고 있었다. 아하! 그럼 그냥 서버로 보낼 데이터를 싹다 모아서 FormData 객체에 넣은 후 전송하면 되겠구나! 하고 또 코드를 수정하고 전송 버튼을 누르는 순간 역시 실패했다.🤭

같이 이미지 저장 부분을 개발하시던 백엔드 개발자분도 이미지를 주고받는 부분의 경험이 없었고, 간단하게 생각했던 부분이 프로젝트 전체에서 가장 어려운 부분이 되었다. FormData에 데이터 하나씩도 넣어보고, 모든 데이터를 객체로 묶어서 한번에 넣어보고.. 의미없는 수정이 반복되었다.

사실 FormData에 대해 잘 알지도 못하면서 되길 바라는게 욕심이라고 생각했고, FormData와 관련된 지식의 공부가 우선되어야 함을 느낀 후 관련 글들을 읽으면서 등장하는 키워드들을 정리해나갔다. (FormData, Content-Type, XMLHttpRequest, AJAX...등) 그리고 키워드 하나하나에서 시작해서 전체 흐름을 이해할 수 있을만큼 공부하여 정리해보았다.

여러 키워드들에서 XMLHttpRequest가 등장해서, 이것부터 이해해해야 다음으로 넘어갈 수 있겠구나 싶어 여기서부터 시작했다.


XMLHttpRequest

출처 : https://developer.mozilla.org/ko/docs/Web/API/XMLHttpRequest

XHR객체는 서버와 상호작용할때 사용한다.

XHR을 이용하면 페이지의 새로고침 없이 데이터를 요청하거나 받아올 수 있다. 따라서 웹페이지 전체를 다시 로딩하지 않고 일부분만을 업데이트 할 수 있는 것이다. (여기서 AJAX를 떠올릴 수 있다)

XMLHttpRequest는 XML뿐만 아니라 모든 종류의 데이터를 가져올 수 있다.

생성자

let xmlHttp = new XMLHttpRequest()

XMLHttpRequest 객체를 생성한다.

Properties

XMLHttpRequest.readyState

XMLHttpRequest 객체의 현재 상태를 나타낸다.

readyState 값은 XHR 객체의 현재 상태에 따라 다음과 같은 주기로 변화한다.

valuestate설명
0UNSENTXMLHttpRequest 객체가 생성됨. open()메서드는 아직 호출되지 않음.
1OPENEDopen()메서드가 성공적으로 실행됨.
2HEADERS_RECEIVEDsend()메서드가 성공적으로 실행됨.
3LOADING요청한 데이터를 처리 중임.
4DONE요청한 데이터의 처리가 완료되어 응답할 준비가 완료됨.

XMLHttpRequest.status

HTTP response status code를 반환함.

XMLHttpRequest.responseText

응답을 string으로 반환한다. 요청이 실패했거나 요청하지 아직 응답이 오지않은경우에는 null을 반환한다.


Methods

XMLHttpRequest.open(method, url, [async])

요청을 초기화

  • method
    GET, POST와 같은 HTTP request method
  • url
    request를 보낼 url
  • async
    비동기로 동작할건지 여부(default는 true). false로 설정하면 send()는 응답이 오기전까지 반환되지 않고 서버로부터 응답이 오기까지 기다린다. true로 설정하면 event listener를 통하여 완료된 트랜잭션의 알림이 제공된다. 따라서 자바스크립트 함수가 지속적으로 수행될 수 있어 서버로부터 응답을 받기 전에도 유저와의 상호작용을 지속할 수 있다. multipart 속성이 참이면 반드시 이 값도 참이여야한다.(그렇지 않으면 예외 발생)

XMLHttpRequest.send([body])

요청을 전송

서버로 request를 전송한다. 요청이 비동기인 경우(default) send()는 요청을 전송하는 즉시 반환한다. 동기인 경우에는 서버로부터 응답이 도착할때까지 반환되지 않는다.

request body를 지정하는 파라미터를 받는데(optional), 이는 PUT과 같은 request에 사용되며 GET 또는 HEAD인 경우 파라미터는 무시되고 request body는 null로 설정된다.

setRequestHeader()를 통해 Accept 헤더를 설정하지 않은 경우 */*(모든유형)의 Accept 헤더가 전송된다.

  • body
    XHR request로 보낼 데이터의 body.
    파일 업로드와 같이 바이너리 컨텐츠를 전송할때의 최적의 방법은 send()에 TypedArray, DataView 또는 Blob 객체를 함께 사용하는것이다.

XMLHttpRequest.setRequestHeader(header, value)

HTTP 요청 헤더의 값을 설정한다.

반드시 send()보다 먼저, open()보다 뒤에 호출해야한다.
이 메소드를 이용해 Accept 헤더를 설정하지 않으면 send()가 호출될때 Accept 헤더가 */*와 함께 전송된다.

  • header
    설정할 헤더 이름
  • value
    헤더의 body에 설정될 값

XMLHttpRequest를 이해하고 나니 AJAX의 비동기성이 XMLHttpRequest를 이용하기 때문에 가능하다는것을 이해할 수 있었다. 이런 특징이 있구나~에서 멈추고 왜 이런게 가능한지에 대해서 궁금증을 가지지 않은 점에 대해서 반성했고, 다음으로 AJAX에 대해 좀 더 이해하고자 했다.


AJAX

Asynchronous JavaScript And XML

번역하면 비동기 자바스크립트와 XML으로, 서버와 통신하기 위해 XMLHttpRequest 객체를 사용하는 것을 의미한다.

AJAX는 JSON, XML, HTML, 텍스트 등 다양한 포맷을 주고받을 수 있다.

AJAX의 특징은 페이지 전체를 새로고침하지 않는 비동기성이다. 이 덕분에 우리는 이벤트가 발생했을때마다 전체 페이지를 새로고침하지 않고도 업데이트하고자 하는 일부분만을 변경할 수 있는 것이다.

AJAX를 이용하여 우리는 페이지의 새로고침없이 서버에 request를 보내고, response를 받아서 작업을 수행할 수 있는 것이다.

AJAX로 HTTP 통신하기

출처 : https://developer.mozilla.org/ko/docs/Web/Guide/AJAX/Getting_Started

1. HTTP request 생성

Javascript를 이용하여 서버로 요청을 보내기 위해 HTTP request를 생성하려면 XMLHttpRequest와 같이 적절한 객체의 인스턴스가 필요하다. 이러한 로직은 XMLHttp라는 비동기 통신 방식에서 처음으로 ActiveX라는 객체를 이용한것에서부터 시작되었다. 이후 Mozilla, Safari등의 브라우저가 XMLHttpRequest 객체를 적용하기 시작했고, 마이크로소프트도 익스플로어 7 버전부터 지원하게 되었으며 W3C 표준으로 제정되었다. 따라서 현재 대부분의 웹브라우저는 XMLHttpRequest를 이용하고 있다.

따라서 HTTP request를 보내려면 일단 XMLHttpRequest객체가 필요하니 생성자를 이용해 만들어주자.

let httpRequest = new XMLHttpRequest();

서버에 request를 보내기 전에, 서버로부터 응답을 받았을때 어떤 동작을 할것인지 정해둬야 한다. XHR의 readystatechange event를 이용하여 상태가 변화할때마다 특정함수가 호출되도록 하자.

httpRequest.onreadystatechange = alertContents(); //XHR 객체의 state가 변할때마다 alertContents()함수 호출

이제 서버로부터 응답을 받았을때의 동작을 정했으니 요청을 해도 된다. XHR 객체의 open(), send() 메소드를 이용한다.

httpRequest.open('GET', 'http://www.example.org/some.file', true);
httpRequest.send(null);

만약 POST 형식으로 데이터를 전송하려고 한다면 차이가 있는데, 이를 이해하기 위해서는 GET과 POST의 차이도 이해해야 한다. 잠깐 여기로 갔다가 돌아와보자.

GET vs POST

GET은 서버에서 데이터를 받아올때, POST는 데이터를 전송할때 사용한다 라고만 알고 그렇게 사용해왔는데, 이부분에 대해서도 좀 더 깊이 이해하고자 하지 않았던 점에 대해서 반성했다.

출처 : https://hongsii.github.io/2017/08/02/what-is-the-difference-get-and-post/

GET

GET은 서버로부터 정보를 조회하기 위해 설계된 메소드이다.

요청을 전송할때 필요한 데이터를 body에 담지 않고 쿼리스트링(url뒤에 ?가 붙고 이름=값 쌍으로 이루어짐. 파라미터가 여러개라면 &로 연결)을 통해서 전송한다. 따라서 길이에 제한이 있고 url에 데이터가 노출되어 보안 취약점이 있다.

POST

POST는 리소스를 생성하거나 변경하기 위해 설계되었기 때문에 전송해야할 데이터를 HTTP Body에 담아서 전송한다. Http Body는 길이 제한 없이 데이터를 전송할 수 있어 POST는 대용량의 정보도 전송가능하다. 또한 눈에 보이지 않기 때문에 보안적으로 안전하다고 생각할 수 있으나 개발자도구 등으로 요청내용을 확인할 수 있기 때문에 민감한 데이터는 반드시 암호화해서 보내야한다.

또한 **보내는 데이터의 타입을 요청 헤더의 Content-Type에 명시**해야한다. 명시하지 않으면 서버는 데이터 타입을 유추하고, 알수 없다면 application/octet-stream로 처리하게 된다. (새롭게 알게된 부분!)


다시 AJAX로 돌아와서, POST와 GET방식에는 차이가 존재함을 알수있었다. 또한 POST 형식으로 데이터를 전송하려면 데이터 타입을 헤더에 명시해야했다.

AJAX에서도 이 과정이 필요하고, 따라서 만약 POST 형식으로 데이터를 전송하려고 한다면 request에 MIME type을 먼저 설정해야 한다. 따라서 send()를 호출하기 전에 setRequestHeader를 이용하여 send()로 보낼 쿼리를 이용해야한다.

httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

MIME type과 Content-Type

사실 MIME typeContent-Type에 대해서 잘 몰라서 공부했는데, 내용이 잘 정리된 블로그 글이 있어서 주소만 첨부하고 글을 추가하진 않겠다.

간단하게 요약하면 MIME Type미디어타입이라고도 하며 인터넷에서 데이터 형식을 식별하기 위해 사용하는 타입이다. Type/Subtype으로 구성되며 폰트, 영상, 텍스트, 이미지, multipart등마다 각각 다른 MIME Type이 존재한다.

Content-TypeHTTP헤더에 존재하는 파라미터로 어떤 타입의 데이터가 전송되는지 웹 서버 또는 클라이언트에 알리는 역할을 한다.(데이터를 받는 쪽에게 알리는것. 이는 클라이언트&서버 둘 다 될 수 있다.)

둘의 차이는 MIME Type은 인터넷, Content type은 웹에서 사용된다는 점이다. 따라서 MIME Type이 상위개념이라고 이해하면 될듯하다.

아무튼 이제 거의 생소한 개념이 다 이해가 되었으니 진짜로 AJAX로 돌아가자. 지금까지 XHR객체를 생성하고, 응답을 받으면 실행할 함수를 연결했고 드디어 request를 보냈다. 다음으로는 응답을 처리하는 과정이다.

2. Response 처리

1단계에서 request를 보내기 전에 response를 처리할 함수를 미리 지정했었다.(alertContents)

이 함수에서는 먼저 request의 상태를 검사할 필요가 있다.(readyState 이용, 위의 XHR property 참고)

if (httpRequest.readyState === XMLHttpRequest.DONE) {
    // 상태값이 Done이면 서버로부터 모든 response를 받고 이를 처리할 준비가 되었음을 의미
    // 이상 없음, 응답 받았음
} else {
    // 아직 준비되지 않음
}

다음에는 HTTP response status code를 검사해야한다(즉 request, response가 모두 정상적인지 검사하는 과정을 거치는 것). 간단하게 Ajax request가 정상적으로 처리되었는지만을 검사하기 위해 응답코드가 200인지만 검사했다.

if (httpRequest.status === 200) {
    // 이상 없음!
} else {
    // 요구를 처리하는 과정에서 문제가 발생되었음
    // 예를 들어 응답 상태 코드는 404 (Not Found) 이거나
    // 혹은 500 (Internal Server Error) 이 될 수 있음
}

이제 request와 response에 대한 status code를 모두 검사했으므로 원하는 처리를 수행하면 된다.

XHR의 responseText, responseXML등의 속성을 이용하여 응답데이터를 원하는대로 다루면 된다.

추가로, 지금까지의 과정은 open()에서 비동기로 설정했을때에 해당한다. 만약 false값을 줘서 동기로 동작하게 된다면 alertContents()함수를 지정할 필요없이 send() 호출에 의해 반환되는 데이터를 바로 이용하면 된다. 하지만 javascript는 send()를 호출하고 응답이 올때까지 멈추게 되니 유저는 불편함을 겪게 된다.

3. 1+2 = HTTP request 수행 예시

1번과 2번을 합쳐서 실제 HTTP request를 수행하는 예시가 있다. 전체 흐름을 이해하기에 매우 좋은 예제인것같아 넣었다.

<button id="ajaxButton" type="button">Make a request</button>

<script>
(function() {
  var httpRequest;
  document.getElementById("ajaxButton").addEventListener('click', makeRequest);

  function makeRequest() {
    httpRequest = new XMLHttpRequest();

    if(!httpRequest) {
      alert('XMLHTTP 인스턴스를 만들 수가 없어요 ㅠㅠ');
      return false;
    }
    httpRequest.onreadystatechange = alertContents;
    httpRequest.open('GET', 'test.html');
    httpRequest.send();
  }

  function alertContents() {
  try {
    if (httpRequest.readyState === XMLHttpRequest.DONE) {
      if (httpRequest.status === 200) {
        alert(httpRequest.responseText);
      } else {
        alert('There was a problem with the request.');
      }
    }
  }
  catch( e ) {
    alert('Caught Exception: ' + e.description);
  }
}

})();
</script>

먼저 유저가 Make a request 버튼을 클릭하게 되면 버튼의 event lisnter가 makeRequest를 호출한다. maekRequest에서는 XHR객체를 생성하고, 서버로 request를 보내면 state에 따라 alertContents함수가 실행된다. alertContents함수에서는 response가 왔는지, response가 정상적인지 검사하고, 모두 정상적이면 alert로 test.html 파일의 내용을 띄우게 될것이다.


여기까지 이해했을때 진짜 이때까지의 개발과정을 돌아보며 반성했다... HTTP 통신을 주구장창 이용하면서 axios를 어떻게 이용하는지만 알고 있었을 뿐, axios가 XHR객체를 생성하고 이런 과정을 포함하고 있다는 사실을 이제서야 안게 많이 아쉬웠다

암튼!! 여기까지 이해하고 나서 다시 문제상황으로 돌아가보았다. 현재 상황은 이미지와 텍스트 데이터를 동시에 보내야 하는것인데, axios에서는 XHR객체를 생성해서, 이 데이터를들을 send(body)에 담아 서버로 전송(POST)하는 과정을 대신 해주고 있었던 것이다. 그래서 나는 그냥 데이터들을 object에 넣고, axios에 method, url, data만 적어주고 편하게 http 통신을 수행해 올 수 있었던 것...

그렇다면 POST 방식에서는 MIME type을 지정해줘야 했는데, 난 이때까지 아무것도 지정해오지 않았으므로 서버가 타입을 유추해서 사용해왔겠지? 근데 지금은 데이터가 이미지/텍스트 둘 다 있으니까 타입이 서로 다를텐데, 한 request의 body에 다 넣어서 보내면 타입을 구분해서 유추할 수 있나? 유추할 수 없으면 직접 지정해서 서버에게 알려줘야하는데 두가지 MIME type을 가지고 있다~라고 알려줄 수 있을까?

이렇게 생각이 흘러갔다. 결론은 여러가지 MIME type을 가지고 있음을 의미하는 MIME type이 있을까?에 대해 조사해봤다.

출처 : https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/MIME_types

text, image, audio, video, application..등 다양한 type이 존재한다. 그 중 띄었던건 multipart 타입인데,

멀티파트 타입은 일반적으로 다른 MIME 타입들을 지닌 개별적인 파트들로 나누어지는 문서의 카테고리를 가리킵니다. 즉 이 타입은 합성된 문서를 나타내는 방법입니다. HTML Forms과 POST 메서드의 관계 속에서 사용되는 multipart/form-data 그리고 전체 문서의 하위 집합만 전송하기 위한 206 Partial Content 상태 메시지와 함께 사용되는 multipart/byteranges를 제외하고는, HTTP가 멀티파트 문서를 다룰 수 있는 특정한 방법은 존재하지 않습니다

라고 적혀있다.

다른 MIME 타입들을 가진 합성된 문서를 나타내는 타입. 내가 찾던 타입과 동일한 타입이였다. 그럼 multipart 타입을 사용해야하는건 일단 고정이고, multipart 타입 중에서도 HTML Forms과 POST 메서드의 관계 속에서 사용되는 multipart/form-data. 완전히 내 상황과 동일한 설명이다. 그렇다면 나는 request를 보낼때 헤더에 Content-Type이 multipart/form-data라고 명시해줘야 함을 알 수 있었다!

다음으로는 multipart/form-data에 대해서 좀 더 알아봤다.


Multipart/form-data

출처 : https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/MIME_types#multipartform-data

multipart/form-data는 브라우저에서 서버로 HTML Form의 내용을 전송할때 사용한다.

multipart는 문서 형식으로서, 경계(--로 시작되는 문자열)로 각 파트가 구분되어진다. 각 파트는 자신만의 HTTP 헤더를 가지는데, 여기에 Content-Type이 포함된다

<form action="http://localhost:8000/" method="post" enctype="multipart/form-data">
  <input type="text" name="myTextField">
  <input type="checkbox" name="myCheckBox">Check</input>
  <input type="file" name="myFile">
  <button>Send the file</button>
</form>

여기서 보면 form태그의 enctype 속성값으로 multipart/form-data가 사용됨을 알 수 있다.

enctype

HTTP method가 POST인 경우, enctype은 양식 제출 시 데이터의 MIME type을 나타낸다.

  • application/x-www-form-urlencoded: 기본값.
  • multipart/form-data: <input type="file">이 존재하는 경우 사용.
  • text/plain: HTML 5에서 디버깅 용으로 추가된 값.

여기서 enctype속성값이 HTTP 헤더의 Content-Type을 지정하는거구나~를 이해할 수 있었고, form태그로 파일을 전송할때에도 반드시 MIME type을 multipart/form-data로 명시해야함을 알 수 있었다.


정리하자면, 이제 multipart/form-data를 사용해야하는건 알았고, 어떻게 사용해야하는지 알기위해서 좀 더 알아보니 multipart/form-dataHTML Form의 내용을 전송할때 사용했다. HTML의 <form> 태그는 정보를 제출하기 위한 용도로 되고, 이를 이용해 유저의 입력을 받은 뒤 서버로 전송하면 된다.

어쨌든 AJAX로 폼데이터를 전송해야하는데, 이때 FormData를 사용하면 된다고 한다. 처음에는 단순히 multipart/form-data를 이용하려면 무조건 FormData에 데이터를 집어넣어야하나?라고 생각했는데 그것보단 폼데이터들을 AJAX로 전송하기 위해서(XMLHttpRequest에 담아서 전송할 수 있도록)FormData를 사용하는것이라고 보는게 더 맞다고 생각했다.

암튼 FormData에 대해서도 좀 알아보자


FormData

출처 : https://developer.mozilla.org/ko/docs/Web/API/FormData

FormData는 폼을 쉽게 보낼 수 있게 도와주는 객체이다. 이를 이용하면 AJAX로 폼데이터를 전송할 수 있다.(==XMLHttpRequest.send()로 전송하는것) 보통은 HTML <form>태그를 사용해서 전송하지만 이와 동일한 역할을 FormData로 할 수 있다.

일반적으로는 AJAX로 폼 전송을 할 필요가 없다. 그냥 JSON구조로 key/value쌍의 데이터를 전송하는게 일반적이지만, 이미지를 전송해야하거나 새로고침 없이 폼데이터를 전송하고싶을때 사용하면 된다.

FormData()

새로운 FormData 객체를 생성

let formData = new FormData(form)

FormData.append(name, value, [filename])

FormData 객체 안에 이미 키가 존재하면 그 키에 새로운 값을 추가하고, 키가 없다면 추가

append()를 사용하여 key/value쌍을 FormData 객체에 추가한다.

  • name
    value에 포함되는 데이터 필드의 이름
  • value
    필드의 값
  • filename
    Blob 또는 File이 value로 전달될때 서버에 report하는 파일 이름
formData.append(name, value);
formData.append(name, value, filename);

다시 정리하면 나는 FormData에 이미지/텍스트 데이터를 각각 넣은 후 이를 POST방식으로 전송하고, 이때 Content-Type은 multipart/form-data로 지정해 주면 된다.

이때 텍스트들이 담긴 Object는 JSON.stringify()를 이용해 JSON문자열으로 변환한 뒤 FormData에 추가한다. (FormData에는 Blob(File등을 포함하여), String 데이터만 append 가능하다.)

코드를 간단하게 작성해보면

const formData = new FormData();
if (coverImage) formData.append('coverImage', coverImage); //이미지 추가
const archivesSaveRequestDto = JSON.stringify(data);
formData.append('archivesSaveRequestDto', archivesSaveRequestDto); // 텍스트 데이터들 추가
      

const response = await instance.post(`/api/v1/archives`, formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
});

415 Unsupported Media Type

하지만 request를 보내보면...?

여전히 에러가 발생한다...

415 error는 생소했기 때문에 먼저 어떤 에러인지부터 알아봤다.

출처 : https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415

415 Unsupported Media Type 클라이언트 에러 응답 코드는 payload 형식이 지원하지 않는 형식이므로 서버에서 request를 수락하지 않음을 나타낸다.
형식 문제는 request에 표시된 Content-Type, Content-Encoding 또는 data 자체를 검사한 결과일 수 있다.

설명을 읽어보면 request의 payload, Content-Type을 확인해봐아함을 알 수 있다. 그런데 과연 누군가 http request에 대해 물어보거나 payload가 뭔지 질문한다면 내가 대답할 수 있을지 확신이 들지 않아, 먼저 http request가 어떻게 구성되었는지 부터 알고자 했다.


HTTP Message

출처 : https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages

HTTP Message는 서버와 클라이언트 간에 데이터가 교환되는 방식이다. 메세지 타입에는 request/response 두가지가 있다. request는 클라이언트가 서버로 전달하여 서버의 액션이 일어나게 하는 메세지이며, response는 request에 대한 서버의 답변이다.

response/request의 구조는 서로 비슷하다.

HTTP message의 start-line과 HTTP Header를 묶어서 request의 head라고 하며, HTTP message의 payload를 body라고 한다.

  • start-line에는 실행되어야할 요청, 또는 요청 수행에 대한 성공 혹은 실패가 기록된다. 이 줄은 항상 한 줄로 끝난다.

  • 옵션으로 HTTP Header 세트가 들어간다. 여기에는 요청에 대한 설명, 혹은 메세지 본문에 대한 설명이 들어간다.

  • 요청에 대한 모든 메타 정보가 전송되었음을 알리는 empty line이 삽입된다.

  • body에는 요청과 관련된 내용(HTML Form contents)이 옵션으로 들어가거나, 응답과 관련된 document가 들어간다. body의 존재 유무 및 크기는 start line과 HTTP Header에 명시된다.

HTTP request

start-line

HTTP request는 서버가 특정 동작을 취하게끔 만들기 위해 클라이언트에서 전송하는 메세지이다. start-line은 아래 3가지 요소로 이루어져 있다.

  1. HTTP method
    영어 동사/혹은 명사를 사용해 서버가 수행해야할 동작을 나타낸다. ex)GET, HEAD
    GET은 리소스를 클라이언트로 가져다 달라는것을 뜻하며, POST는 데이터가 서버로 들어가야함을 의미한다(리소스를 새로 만들거나 수정하기 위해).

  2. request target
    주로 URL 또는 프로토콜, 포트, 도메인의 절대경로

  3. HTTP version
    메세지의 남은 구조를 결정하기 때문에 response 메세지에서 써야할 HTTP version을 알려주는 역할을 한다.

request에 들어가는 HTTP 헤더는 HTTP 헤더의 기본구조를 따른다. 대소문자 구분없는 문자열 다음에 :가 붙으며, 그 뒤에 오는 값은 헤더에 따라 달라진다. 헤더는 한줄로 구성되지만 값 까지 포함해 꽤나 길어질 수 있다.

다양한 종류의 request 헤더를 몇가지 그룹으로 나눌 수 있다.

  • General headers
    메세지 전체에 적용된다. request/response 모두에 적용된다.

  • Request headers
    request의 내용을 좀 더 구체화 시키고, context를 제공하기도 하며 조건에 따른 제약 사항을 가하기도 하면서 request 내용을 수정한다.

  • Entity headers
    request 본문(body)에 적용된다. request내에 body가 없는 경우 entity 헤더는 전송되지 않음.

Body

body는 request의 마지막 부분에 들어간다. 모든 request에 body가 들어가지는 않는다. GET, HEAD, DELETE, OPTIONS처럼 리소스를 가져오는 요청은 보통 body가 필요하지 않다. 이와 다르게 POST요청과 같이 업데이트를 하기 위해 서버에 데이터를 전송해야하는 request가 있다.

body는 크게 두가지 종류로 나눌 수 있다.

  • single-resource bodies
    헤더 두개(Content-Type, Content-Length)로 정의된 단일 파일로 구성

  • multiple-resource bodies
    멀티파트 본문으로 구성되는 다중 리소스 본문에서는 파트마다 다른 정보를 지니게 된다. 보통 HTML form과 관련있음.


이제 크롬 개발자도구의 network탭에서 어떤 정보를 보여주고 있는지 좀 더 명확하게 이해할 수 있게 되었다.

Header탭에서 General, Request Header를 각각 확인할 수 있다(Entity Header는 Request, Response Header에 포함되어 있는 것 같다).

그리고 415 에러는 request의 payload가 원인일 수 있다고 했으니 payload를 확인해봤다.

여기서 이상하다고 생각했던 점은, 이미지는 Content-Type이 image/png로 잘 나타나는것 같은데 JSON 데이터는 Content-Type이 나타나있지 않았다. 그렇다면 서버는 JSON 데이터를 기다리고 있는데, 데이터 타입을 application/octet-stream로 처리해서(데이터 타입을 유추할 수 없으면 application/octet-stream로 처리) Media Type 에러가 발생하는것은 아닐까? 라는 생각이 들었고, 그렇다면 JSON데이터임을 알려주면 되지 않을까? 생각했다.

그렇게 하기 위해서는 multiple/form-data로 content-type이 지정된 상태에서 그 안의 데이터들이 각각 다른 content-type을 가지고 있어야한다. 이걸 임의로 지정해줄 수 있는지 검색해본 결과 해결방법을 찾을 수 있었다.

출처 : https://stackoverflow.com/questions/24535189/composing-multipart-form-data-with-a-different-content-type-on-each-parts-with-j

FormData에 데이터를 넣을 때 Blob을 이용해서 구체적인 Content-Type을 지정해줄 수 있다고 한다.


Blob

출처 : https://developer.mozilla.org/ko/docs/Web/API/Blob

Blob이란 Binary Large Object로, 파일류의 미가공 데이터를 나타낸다. Blob()을 이용해 Blob이 아닌 데이터로 Blob을 생성할 수 있다. 이때 option으로 Blob에 저장할 데이터의 MIME type을 지정할 수 있다.

const array = ['<a id="a"><b id="b">hey!</b></a>']; // 하나의 DOMString을 포함한 배열
const blob = new Blob(array, {type : 'text/html'}); // 생성한 블롭

따라서 FormData에 JSON문자열을 넣는게 아니라, Blob객체를 넣은 후 type을 JSON으로 지정해주자.

const formData = new FormData();
if (coverImage) formData.append('coverImage', coverImage); //이미지 추가
const archivesSaveRequestDto = JSON.stringify(data);
formData.append('archivesSaveRequestDto', new Blob([archivesSaveRequestDto], {type:'application/json'})); // 텍스트 데이터들 추가
      

const response = await instance.post(`/api/v1/archives`, formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
});

이렇게 수정하면 드디어

201을 확인할 수 있다..


각 데이터들의 Content-Type도 원하는대로 잘 적혀있다.


반성의 시간

이렇게 오래걸릴 부분인줄 몰랐지만, 정말 이부분에서 프로젝트 개발의 반을 소요할정도로 오래 걸렸다. 사실 구글링을 통해 해결방법이 담긴 코드를 그대로 따라작성하는것으로 문제해결을 끝마칠 수도 있지만, 그 속에 담긴 여러 지식들을 이해하는 과정을 통해 정말 많은것을 배웠고 또 정말 많은것을 모르고 있다는 사실을 반성했다. 이번 과정을 통해 기억해야할 점을 정리해보면

  • 개발할때 모르는 부분은 단순히 구글링을 통해 복붙하지 않기
    개발하면서 에러가 발생하거나 모르는게 생기면 구글링을 통해 해결할 수 있는 코드를 찾아서 바로 사용해보고, 해결되면 그냥 넘어갔던 순간이 많았던 것 같다. 비록 시간은 조금 더 걸리더라도 내가 어떤 부분을 몰랐고 부족했는지 알아내고 이를 채워가는 과정이 훨씬 값어치 있을 것 같다.

  • MDN 웹문서 자주 읽기
    Javascript를 처음 공부할때도 MDN 웹문서를 이용했었는데, 단순한 지식말고도 정말 깊고 다양한 웹 지식이 담겨있고 신뢰도 높은 정보를 얻을 수 있어 모르는게 생기면 거의 항상 여기서 찾아봤다. 모르는게 없을때도 심심할때 종종 글들을 읽어보면 많은 도움될 것 같다.

  • 블로그 글 많이 작성하기
    개념을 이해하는것과 이해한 내용을 글로 풀어쓰는 과정은 또 다른 것 같다. 글로 작성하다보면 막연하게 이해했던 내용에서는 막히게 되고, 이때 다시 한번 공부할 수 있어서 좋은 것 같다.

  • 개념서 읽기
    개발을 늦게 시작한만큼 개발지식을 습득하기에 바빠서 학교수업 외에는 CS공부를 소홀히 한 것 같다. 이 과정을 통해 정말 개념의 중요성을 깨닫고, 기본을 탄탄히 할수록 개발할때 더 많은 것들을 볼 수 있음을 느꼈다. 마침 SW마에스트로에서 책 구매를 지원해줘서 개념서들을 왕창 구매했다. 하나씩 꼼꼼히 읽어가며 부족한 부분을 얼른 채워나가고 싶다.

0개의 댓글