JavaScript WEB - Content-type들(심화)

김재환·2023년 11월 15일

JavaScriptWEB

목록 보기
12/27

이제 우리는 리퀘스트와 리스폰스의 바디에 정말 다양한 타입의 데이터들이 들어갈 수 있다는 것을 배웠습니다. 이때까지는 실제로 개발자가 되면 주로 사용하게 될 JSON 타입을 많이 다뤄봤는데요. 하지만 정말 개발자가 된다면 JSON 뿐만 아니라 이전 노트에서 봤던 일반 텍스트, 이미지, 음성, 영상 등 수많은 타입들을 다루게 될 겁니다. 이번 노트에서는 여러분이 추가적으로 알아두면 좋을 데이터 타입들을 공부해보겠습니다.

1. JSON 말고 XML도 있어요.

개발자들이 어떤 정보를 나타내기 위해 흔히 쓰는 데이터 포맷으로는 JSON 뿐만 아니라 XML(Extensible Markup Language)이라고 하는 데이터 포맷도 있습니다. XML을 한마디로 쉽게 이야기하자면, 태그를 사용해서 데이터를 나타내는 것입니다. 예를 들어

{
   "name":"Michael Kim",
   "height":180,
   "weight":70,
   "hobbies":[
      "Basketball",
      "Listening to music"
   ]
}

이런 JSON 데이터를 XML로는 이렇게 나타낼 수 있습니다.

<?xml version="1.0" encoding="UTF-8" ?>
<person>
    <name>Michael Kim</name>
    <height>180</height>
    <weight>70</weight>
    <hobbies>
        <value>Basketball</value>
        <value>Listening to music</value>
    </hobbies>
</person>

뭔가 HTML에서나 볼 법한 태그들로 이루어져있죠? 자세히 보면 원래 JSON에서

"name": "Michael Kim"

이라고 나타낸 부분을 XML에서는


<name>Michael Kim</name>

이런 식으로 시작 태그()와 끝 태그(), 그리고 그 사이의 값으로 나타낸 것을 알 수 있습니다.

사실 XML이라는 데이터 타입은 JSON이 2013년에 표준화되고 그 뒤로 활성화되기 전까지만 해도 정말 많이 사용되던 데이터 타입이었습니다. 여러분이 개발 관련 문서들을 구글링하다보면 여전히 이 XML로 표현된 데이터들을 자주 볼 수 있게 될 텐데요.

XML을 쓸 때는 보통 스키마(Schema)라는 별도의 문서를 함께 사용합니다. 이 스키마에는 각 조직, 기관 등에서 XML로 데이터를 나타낼 때, 어떤 태그들을 사용할 수 있고, 각 태그의 의미는 무엇이며, 특정 태그는 어떤 타입의 값을 가질 수 있는지 등의 정보가 담겨있는데요. 따라서 XML은 데이터에 대한 엄격한 유효성(validity) 검증에 특화된 데이터 포맷이라고 할 수 있습니다.

하지만 XML은 같은 양의 데이터를 표현하더라도 JSON에 비해 더 많은 용량을 차지하고, JSON에 비해 가독성이 떨어지며, 배우기가 어렵다는 문제 등으로 인해, 오늘날 XML의 입지는 다소 좁아진 것이 사실입니다. 특히나 자바스크립트가 중심이 되는 웹 개발 세계에서는 우리가 배운 것처럼 자바스크립트의 문법과 JSON 문법이 대체로 호환되기 때문에 더더욱 JSON을 사용하는 것이 편리합니다.

하지만 만약 여러분이 외부로 공개된 여러 Open API 같은 것들을 살펴보면 여전히 XML 타입의 데이터를 리스폰스로 주는 경우가 많다는 것을 알 수 있습니다. 그렇기 때문에 XML 타입이라는 것이 존재한다는 것을 인지하고, 이런 타입의 데이터는 어떻게 처리해야 할지 미리 고민해보는 것도 좋습니다. 참고로 XML을 나타내는 Content-Type 헤더의 값은 'application/xml'입니다. 그리고 'application/xml'뿐만 아니라 XML의 문법을 따르되 거기에 특수한 규칙을 더해 만든 데이터 타입들도 존재합니다. 보통 이런 타입들은 그 이름 끝에 +xml을 붙여서 사용하는데요. Content-Type 헤더의 값에 관한 이 공식 문서에 접속해서 +xml 이라는 키워드로 페이지 내 검색을 해보세요. XML 문법을 활용한 다양한 데이터 타입들을 볼 수 있을 겁니다.

2. form 태그에서 사용되는 타입들

이때까지 배운 JSON, XML 이런 것들 말고도 개발자라면 알아둬야 할 데이터 타입이 또 있습니다. 그것은 바로

(1) application/x-www-form-urlencoded 타입
(2) multipart/form-data 타입

이 2가지인데요. 각각의 타입에 대해 순서대로 알아봅시다.

(1) application/x-www-form-urlencoded

뭔가 굉장히 긴 이름의 타입이죠? 이 타입은 우리가 HTML의 form 태그(

)를 사용할 때 종종 보게되는 타입입니다. form 태그는 회원가입 화면이나 게시물 업로드 화면 등을 만들 때 주로 활용되는 HTML 태그인데요. form 태그를 사용하면 자바스크립트 코드 없이 오로지 HTML만으로도 리퀘스트를 보내는 것이 가능합니다. 오늘날에는 form 태그를 사용하지 않고 자바스크립트 코드로 직접 사용자의 입력값을 취합해서 리퀘스트를 보내는 방법이 많이 사용되고 있지만 여전히 form 태그만으로 리퀘스트를 보내는 방식도 쓰이고는 있기 때문에 알아두는 게 좋습니다.

form 태그는 기본적으로 이 application/x-www-form-urlencoded 타입의 데이터를 바디에 담아서 보내는데요.

그렇다면 application/x-www-form-urlencoded 타입은 데이터를 어떤 식으로 나타내는 걸까요? 예를 들어, JSON으로는 다음과 같이 표현할 수 있는 데이터가 있다고 가정해봅시다.

{
  "id": 6,
  "name": "Jason",
  "age": 34,
  "department": "engineering"
}

이 데이터를 application/x-www-form-urlencoded 타입으로 나타내보면

id=6&name=Jason&age=34&department=engineering

이것과 같습니다. application/x-www-form-urlencoded 타입은 프로퍼티의 이름과 값을 "이름=값" 형식으로 나타내고 각각의 프로퍼티를 "&" 기호로 연결하는 방식으로 데이터를 표현하는데요. URL의 query 부분에서 사용하는 방식과 똑같죠? 자, 예시를 통해 좀더 배워봅시다.

예를 들어, 이런 식으로 CodeitShopping이라는 사이트의 회원 가입 페이지가 있다고 해봅시다.

이 웹 페이지는 다음과 같은 HTML 코드로 이루어져 있는데요.

...

<body>
  <div id="signup">
    <p id="title">CodeitShopping</p> 
    <form action="/upload" method="get" enctype="application/x-www-urlencoded">
      <div>
        <div><span class="label">email</span></div>
        <input class="input" type="text" id="email" name="email">
      </div>
      <div>
        <div><span class="label">password</span></div>
        <input class="input" type="password" id="password" name="password">
      </div>
      <div>
        <div><span class="label">nickname</span></div>
        <input class="input" type="text" id="nickname" name="nickname">
      </div>
      <div>
        <input id="submit-btn" type="submit" value="Sign Up">
      </div>
    </form>
  </div>
</body> 
 
...

(CSS, JavaScript 코드는 생략되어 있습니다.)

지금 form 태그의 method라는 속성의 값으로 get이, enctype이라는 속성의 값으로 application/x-www-form-urlencoded라고 써있는 게 보이시나요? 이렇게 속성을 적으면, 나중에 이 form 태그가 리퀘스트를 보낼 때, 리퀘스트의 메소드를 GET으로 설정하고 사용자가 입력한 데이터를, URL의 쿼리 부분에 application/x-www-urlencoded 타입으로 넣습니다. 지금 enctype 속성을 저렇게 설정을 해줘도 되고, 설정을 안 해줘도 form 태그는 기본적으로 application/x-www-urlencoded 타입을 사용합니다. 제가 위 이미지와 같이 회원 정보를 작성하고 아래의 Sign Up(가입하기) 버튼을 누르면 어떤 리퀘스트가 보내질까요? 잠깐 이 이미지를 봅시다.

이 이미지는 제가 회원 가입 버튼을 누르고 웹 브라우저의 주소창 부분을 봤을 때의 결과인데요. 제가 입력한 정보가 키=정보&키=정보&키=정보.. 이런 형식으로 나타난 것을 알 수 있습니다. form 태그는 바로 이렇게 사용자의 입력값을 URL의 query 부분에 application/x-www-form-urlencoded 타입으로 나타낸 URL로 리퀘스트를 보내는 겁니다. 별로 어렵지 않죠?

그런데 잠깐 지금 빨간색으로 표시된 부분을 보면 우리가 작성하지 않았던 이상한 퍼센트(%)기호와 숫자들이 보입니다. 이게 뭘까요? 지금 보면

입력했던 실제 글자 표시된 내용
@ %40
! %21
공백 +

이렇게 변환되어서 표시된 것을 알 수 있는데요. 왜 특정 문자들은 이런 식으로 변환된 걸까요? 바로 이것이 application/x-www-form-urlencoded 타입의 특징인데요.

사실 이건 URL encoding(URL 인코딩)이라는 작업을 수행한 결과입니다. URL 인코딩이란 URL에서 특정 특수문자들 그리고 영어와 숫자를 제외한 다른 나라의 문자들을 이런 식으로 변환하는 것을 말합니다. 왜 이런 작업이 필요할까요? URL 관련 표준에 따르면, URL을 처리할 때, 특정 조건에 해당하는 문자들은 Percent encoding이라는 것을 하도록 되어 있습니다. 이 Percent encoding이 바로 URL encoding인데요. 어떤 경우에 Percent encoding을 해야하는지 알아보겠습니다.

일단, 아래와 같은 특수 문자들은 URL에서 특별한 용도를 갖고 있는 문자들입니다. 이런 특수 문자들이 각자 자신의 원래 용도가 아닌 다른 용도로 사용되는 경우 Percent Encoding을 해줘야 합니다.

: / ? # [ ] @ ! $ & ' ... ' ' (공백)
%3A %2F %3F %23 %5B %5D %40 %21 %24 %26 %27 ... %20 또는 +
그러니까 이런 기호들이 URL에서 본래의 용도로 사용되는 게 아니라, 어떤 데이터를 나타내기 위해 사용된다면 이때는 Percent encoding을 해서 나타내줘야 한다는 뜻입니다. 방금 전 봤던 @, !, 공백 이 글자들도 이 표에 속하고, 본래의 용도가 아닌 데이터를 나타내기 위한 용도로 쓰였기 때문에 Percent encoding 되었던 것입니다.

그럼 왜 이런 Percent Encoding이 필요한 걸까요? 그건 바로 URL에 대한 해석 오류를 방지하기 위해서입니다. 예를 들어, 어떤 URL의 query 부분에 이런 내용이 있다고 생각해보세요.

책 제목이 Tom&Jerry(톰과 제리)라고 되어 있고, 제목 안에 & 가 있습니다. 그런데 이 &는 URL의 query에서 각각의 속성을 구분하는 용도로 쓰이는 기호이기도 하죠? 하지만 이 상태로 그대로 URL을 표현하면 서버가 URL의 path 뒷 부분을 해석하다가 오류를 발생시킬 수도 있습니다. 이 URL을 읽는 개발자가 오해해서 실수하기도 쉽구요. 따라서 본래 용도가 아니라 단지 데이터를 나타내기 위해서 사용된 &은 위 표에 따라 %26으로 변환해주도록 한 겁니다. 이런 식으로 말이죠.

왜 Percent encoding이 필요한지 아시겠죠?

자, 이번엔 Percent encoding을 해야하는 다른 경우를 보겠습니다. 방금 본 특수 문자들의 경우뿐만 아니라 URL에서 영어와 숫자를 제외한 다른 나라 문자를 나타낼 때도 Percent encoding을 해줘야합니다. 그러니까 한국어, 중국어, 아랍어 등을 나타낼 때는 Percent encoding을 해줘야 한다는 뜻입니다.
예를 들어, 우리가 URL의 맨 뒤에

5aqu49hd6-Untitled 2.png

이런 식으로 '코딩'이라는 한글을 적으면 어떻게 될까요? 이 URL을 그대로 복사해서 텍스트 에디터에 붙여넣기해보면

이런 식으로 한국말로 적은 '코딩'이라는 부분이 '%EC%BD%94%EB%94%A9'로 변환된 것을 알 수 있습니다. 따라서 이 리퀘스트를 받는 서버가 리퀘스트에서 찾게되는 path 정보도 바로 이렇게 변환된 결과일 것입니다.

이렇게 URL에서

(1) 'URL 안에서 미리 정해진 용도를 가진 특수 문자를 다른 용도로 사용'하거나
(2) '영어와 숫자'를 제외한 다른 나라 문자를 나타낼 때는

Percent encoding을 해주는 것이 정해진 규칙입니다. 그럼 어떤 식으로 Percent encoding을 해야하는 걸까요? 여기서부터는 조금 어려운 내용이니까 건너뛰셔도 됩니다. 일단 Percent encoding을 하려면 해당 문자를 UTF-8이라고 하는(하나의 문자를 1과 0의 조합으로 나타낼 때 사용하는 규칙) 인코딩 규칙을 적용하여 1과 0의 조합으로 인코딩하고, 한 바이트 당 그 앞에 퍼센트(%) 기호를 붙여주면 됩니다. 한글은 한 글자가 3바이트로 표현되기 때문에 방금 전 '코딩'이라는 단어는, '2글자 X 3바이트'를 해서 총 6개의 퍼센트가 붙은 결과('%EC%BD%94%EB%94%A9')로 변환된 것입니다. 일단 이 말이 무슨 뜻인지 이해가 안 되면 넘어가셔도 됩니다. 이 부분은 나중에 다른 토픽에서 '유니코드', '인코딩' 이런 개념들을 다시 배우고 살펴봅시다. 일단은 URL 표준에 따라 이렇게 URL에서 어떤 문자들을 Percent encoding해야 하는 경우가 있다는 사실만 기억하세요.

바로 이런 URL encoding의 원리를 그대로 반영한 데이터 타입이 바로 application/x-www-form-urlencoded 타입입니다. 왜 지금 이름에 urlencoded라고 하는 단어가 붙어있는지, 이해가 되시죠? 참고로 form 태그에서 method 속성을 get이 아닌 post로 바꾸면 다음과 같이 해당 타입의 데이터가 URL의 query 부분에 표시되는 것이 아니라 리퀘스트의 바디에 들어가도록 할 수도 있습니다.

자, 이제 application/x-www-form-urlencoded 타입이 어느 정도 이해되시죠?

그런데 사실 요즘은 방금 전 회원 가입 페이지처럼 form 태그만으로 리퀘스트를 보내기보다는 자바스크립트로 직접 리퀘스트를 보내는 경우가 많습니다. 게다가 우리가 배운 JSON(application/json)보다 이 application/x-www-form-urlencoded 타입이 딱히 더 좋아보이도 않는데요. 하지만 문제는 웹의 초창기부터 form 태그만으로 리퀘스트를 보내는 코드가 너무 많이 작성되었다는 것입니다. 따라서 여전히 많은 레거시(legacy) 서비스의 서버에서 이 타입을 요구하고 있는데요. (그리고 기술적인 측면에서는, 이것은 좀 어려운 내용이긴 하지만 특정 도메인에서 다른 도메인으로 리퀘스트를 보낼 때(CORS 문제) Content-Type의 값이 x-www-form-urlencoded인 GET 리퀘스트는, 다른 Content-Type 값들에 비해 주고받아야하는 리퀘스트와 리스폰스의 수가 더 적다는 약간의 장점이 있기는 합니다. 이해가 안 된다면 일단 넘어갑시다.)
개발자는 어느 환경에서 개발하게 될 지 모르기 때문에 이 application/x-www-form-urlencoded 타입에 대해서도 잘 알아둬야 나중에 당황하지 않을 수 있습니다.

그리고 form 태그를 사용하지 않고 자바스크립트 코드로 직접 application/x-www-form-urlencdoed 타입의 데이터를 리퀘스트의 바디에 넣는 것도 가능한데요. 예를 들어 다음과 같습니다.

const urlencoded = new URLSearchParams();
urlencoded.append('email', 'tommy@codeit.kr');
urlencoded.append('password', 'codeit123!');
urlencoded.append('nickname', 'Nice Guy!');

fetch('https://learn.codeit.kr/api/members', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: urlencoded,
})
  .then((response) => response.text())
  .then((result) => {
    console.log(result);
  });

이 코드를 사용하면 방금 전과 동일하게 리퀘스트를 보낼 수 있는데요. URLSearchParams라는 객체를 사용하면 자동으로 값에 URL encoding을 적용해주기 때문에, application/x-www-form-urlencoded 타입의 데이터를 fetch 함수로도 손쉽게 보낼 수 있습니다. 참고로 알아두세요.

(2) multipart/form-data

이 컨텐츠 타입은 실무적으로 굉장히 중요한 타입입니다. 이때까지 우리가 살펴본 Content-Type 값들은, 하나의(Single) 데이터의 타입을 나타내는 값들이었습니다. text/html, vidoe/mp4, application/json 등 모두 데이터 하나의 타입을 나타냈었죠? 그런데 이 multipart/form-data는 좀 특별합니다. multipart(여러 개의 파트)라는 단어에서도 유추할 수 있듯이 이 값은 여러 종류의 데이터를 하나로 합친 데이터를 의미하는 타입입니다.

그럼 언제 이런 값이 필요할까요? 잠깐 우리가 게시판에 게시글을 올릴 때를 생각해봅시다. 우리는 글의 제목과 내용을 적고, 이미지 파일이나 영상 파일을 첨부하기도 합니다. 이때 우리가 '게시글 업로드' 버튼을 누르면 파일들의 내용도 리퀘스트에 함께 담겨서 가야할텐데 이때 Content-type의 값은 무엇이어야 할까요? 바로 이럴 때 사용되는 것이 multipart/form-data입니다. 이번에도 예를 통해 배워봅시다.

방금 위에서 봤던 CodeitShopping의 회원가입 페이지가 이렇게 바뀌었다고 해봅시다. 이제 프로필 이미지도 추가로 받는 것으로 바뀌었는데요.

이제 이 페이지에서는 email, password, nickname 같은 텍스트 정보뿐만 아니라 프로필 이미지 파일도 함께 보내줘야합니다. 어떻게 해야 할까요?

이 multipart/form-data 타입의 데이터도 위에서 살펴본 application/x-www-form-urlencoded 타입 때처럼

(1) form 태그만으로도 그리고
(2) 자바스크립트 코드만으로도

리퀘스트의 바디에 담아 전송할 수 있습니다. 순서대로 해보겠습니다.

이 화면은 현재


...
<body>
  <div id="signup">
    <p id="title">CodeitShopping</p> 
    <form action="/upload" method="post" enctype="multipart/form-data">
      <div>
        <input id="image" type="file" name="file" accept="image/*">
        <div id="profile">
          <div id="plus">+</div>
        </div>
       </div>
      <div>
        <div><span class="label">email</span></div>
        <input class="input" type="text" id="email" name="email">
      </div>
      <div>
        <div><span class="label">password</span></div>
        <input class="input" type="password" id="password" name="password">
      </div>
      <div>
        <div><span class="label">nickname</span></div>
        <input class="input" type="text" id="nickname" name="nickname">
      </div>
      <div>
        <input id="submit-btn" type="submit" value="Sign Up">
      </div>
    </form>
  </div>
</body>
... 

이런 HTML 코드로 이루어져 있는데요. (CSS, JavaScript 코드는 생략되어 있습니다.)
이번엔 form 태그의 enctype 속성으로 application/x-www-form-urlencoded가 아니라 multipart/form-data가 써있는 것이 보이시죠?

이번엔 자바스크립트 코드로 하는 방법도 봅시다.


...

const formData = new FormData();
formData.append('email', email.value);
formData.append('password', password.value);
formData.append('nickname', nickname.value);
formData.append('profile', image.files[0], "me.png");

fetch('https://learn.codeit.kr/api/members', {
  method: 'POST',
  body: formData,
})
  .then((response) => response.text())
  .then((result) => { console.log(result); });

이 코드에서 보이는 것처럼, 자바스크립트 코드에서 multipart/form-data 타입의 데이터를 보내려면 FormData라는 객체를 사용해야합니다. 이 FormData를 사용하면 리퀘스트의 Content-Type 헤더의 값을 multipart/form-data로 직접 설정하지 않아도 자동으로 설정해줍니다. 일단은 이렇구나 정도만 이해하고 넘어가주세요.

자, 이제 핵심 내용이 등장합니다. 그럼 multipart/form-data는 실제로 어떻게 생겼는지 개발자 도구에서 보겠습니다.
위의 방법 중 하나를 선택해서 리퀘스트를 보내면, 아래와 같은 리퀘스트가 보내집니다.

일단 리퀘스트의 헤드부터 봅시다. 지금 Content-Type 헤더의 값에 multipart/form-data라고 쓰여있죠? 그런데 그 옆에 쓰여있는 boundary라는 건 뭘까요? boundary는 '경계선'이라는 뜻인데요. 이게 뭔지는 리퀘스트의 바디 부분을 보면 알 수 있습니다. 화면 하단의 Form Data라고 쓰여있는 부분에서 view source를 클릭하면

이렇게 바디에 담긴 multipart/form-data 타입의 데이터를 볼 수 있습니다. 자, 이 내용을 좀 더 보기 편하게 처리해서 보여드리겠습니다.

지금 보면 총 4개의 데이터(email, password, nickname, profile)가 들어가있고 각각의 데이터는

------WebKitFormBoundaryHu7uI1OMKko1vxwV

이 문자열을 기준으로 나뉘어 있다는 것을 알 수 있는데요. 어, 그런데 이 문자열 어디서 보지 않았나요? 방금 전 Content-Type 헤더의 값에서 multipart/form-data 뒤에 적혀있던 boundary의 값이었습니다.

content-type: multipart/form-data; boundary=---------WebKitFormBoundaryHu7uI1OMKko1vxwV

왜 이런 boundary라는 부가 정보가 붙어있는 걸까요? multipart/form-data 타입의 데이터는 그 안에 여러 종류의 데이터들이 들어있다고 했습니다. 따라서 서버가 이것을 받았을 때 각 데이터를 구분할 수 있도록 이런 boundary 값이 필요한데요. 지금 위에서 boundary를 기준으로 각각의 데이터가 나뉘어있다는 것을 알 수 있습니다.

자, 이제 boundary로 구분된 각 영역의 데이터도 살펴봅시다. 지금 1, 2, 3번 데이터를 살펴보면

Content-Disposition: form-data; name=데이터의 이름
// blank line
값

이런 형식으로 저장되어 있는 것을 볼 수 있습니다.

그리고 가장 마지막에, 프로필 이미지에 해당하는 4번 데이터는

Content-Disposition: form-data; name="profile"; filename="me.png"
Content-Type: image/png
// blank line
값

이런 식으로 filename에 실제 이미지 파일의 원래 이름이 있고, 또 그것만의 Content-Type 헤더의 값으로 image/png가 설정되어 있는 것을 볼 수 있습니다. (값의 영역에는 원래 해당 이미지 파일의 바이너리 데이터가 존재하지만, 개발자 도구가 이를 보여주지 않는 것으로 추정됩니다)

정리하면, multipart/form-data 타입은 여러 데이터를 하나로 묶어서 리퀘스트의 바디에 담아보내려고 할 때 사용되는 아주 중요한 타입입니다. 실제 웹 서비스를 떠올려보면, 우리가 회원가입을 하든, 게시글을 업로드하든 다양한 데이터를 한번에 묶어서 보내는 경우가 많죠? 실제로 개발을 할 때도 자주 사용하게 되는 타입이니까 꼭 기억해두세요.

자, 이때까지 Content-Type 헤더의 값 중 하나인 application/x-www-form-urlencoded 타입과 multipart/form-data 타입에 대해 배웠는데요. 혹시 이 내용들 중에 당장 이해가 되지 않는 부분이 있어도 괜찮습니다.

하지만 이 타입들의 존재조차도 모르고 개발 실무로 가면 많이 헤맬 수 있기 때문에, 여러분의 시간 낭비를 줄여드리고자 미리 알려드립니다. 이 두 가지 타입이 존재한다는 것 정도만 기억하시고, 필요한 경우에 좀더 자세히 공부해보세요. 혹시 각 타입에 대해 좀더 깊게 공부하고 싶은 분들은 아래 링크를 참조하시기 바랍니다.

관련 공식 문서

POST 메소드
Content-Type
Form

profile
안녕하세요

0개의 댓글