[기초부터 완성까지, 프런트엔드] 9장. 네트워크 통신

Posted by : on

Category : FrontendBook


HTTP란?

HTTP(HyperText Transfer Protocol)는 HTML과 같은 하이퍼미디어 문서를 전송하기 위한 프로토콜이다. 웹에서 이뤄지는 데이터 통신의 기초이며 주로 TCP를 사용한다. 기본 포트는 80번이며 HTTP를 통해 송수신하는 데이터는 http:로 시작하는 URL로 접근할 수 있다.

프로토콜 ?
프로토콜은 데이터가 전송되는 방식을 결정하는 규악이다. 송신자와 수신자 사이의 합의를 통해 ‘데이터의 포맷은 이렇다’, ‘오류 제어는 이렇게 해야 한다’ 같이 서로 통신을 할 때 이해할 수 있는 규칙을 만든 것이다.

클라이언트 측과 서버는 프로토콜을 통해 정해진 포맷에 맞춰 통신한다. 클라이언트에서 서버로 전송하는 메시지를 요청(request), 이에 대한 답으로 서버에서 클라이언트 측으로 전송하는 메세지를 응답(response)이라 한다.

HTTP 통신은 상태(state)라는 개념이 존재하지 않는다. 즉 클라이언트와 서버가 연결되어 있지 않아 그 전에 처리된 요청과 응답이 통신에 영향을 주지 않고 각각 독립적으로 처리된다. 이런 특징 덕분에 서버 디자인을 단순하게 할 수 있고, 송수신되는 정보 처리를 위한 공간을 동적으로 할당할 수 있으며 이를 제거해 줄 필요가 없다는 장점이 있다. 하지만 요청마다 필요한 정보를 모두 담아 보내야 한다는 단점이 존재한다.

현재는 보안이 강화된 HTTPS(HyperText Transfer Protocol over SSL)를 주로 사용한다. https의 경우 기본 포트로 443을 사용하며 https:로 시작하는 URL을 통해 접근할 수 있다.

HTTP 버전은 0.9, 1.0을 거쳐 1997년 등장한 1.1로 몇십 년간 꾸준하게 확장되어 안정성을 유지했다. 이후 웹 애플리케이션이 점점 복잡해져 감에 따라 더 나은 성능을 위해 2015년 HTTP/2가 표준화되었다. 그리고 현재 TCP 대신 UDP를 사용하는 HTTP/3도 활발하게 개발되고 있다.

TCPUDP
>
네트워크의 7계층 중 네 번째 계층인 전송(transmission) 계층에서 사용되는 프로토콜이 있다. 전송계층은 송신자와 수신자를 연결하는 통신서비스를 제공하는 계층으로, 쉽게 말해 데이터의 전달을 담당한다. 그 중 데이터를 보내기 위해 사용하는 프로토콜이 TCPUDP이다.
> TCP(Transmission Control Protocol)의 경우 연결 지향적이며 순서가 보장되지만 상대적으로 느리며, UDP(User Data Protocol)의 경우 비 연결 지향적이며 순서가 보장되지 않지만 빠른 장점이 있다.

상태 코드

HTTP 응답 상태 코드는 HTTP 요청이 성공적으로 완료되었는지 알려주는 세 자리 정수이다. 응답은 다섯 개의 그룹으로 나뉘며 상태 코드를 통해 성공 여부 뿐만 아니라 리다이렉트, 클라이언트, 서버의 에러에 관련된 정보를 알 수 있다. 이 응답을 통해 사용자에게 적절한 안내를 할 수 있으며 사이트의 동작을 제어할 수 있다.

  • 1xx(Informational) : 서버에서 요청을 수신했으며 현재 처리하고 있거나 정보를 알려 줄 필요가 없을 경우
  • 2xx(Successful) : 요청이 성공적으로 완료된 경우
  • 3xx(Redirection) : 요청을 마치기 위해 다른 위치로 리다이렉션하는 것과 같이 추가로 동작을 취해야 할 경우
  • 4xx(Client Error) : 요청에 잘못된 구문이 포함되어 있거나 수행할 수 없는 경우
    • 400(Bad Request) : 요청 오류. 잘못된 문법으로 인해 서버가 해당 응답을 이해할 수 없는 경우
    • 401(Unauthorized) : 권한 없음. 요청을 받기 위해서 인증을 받아야하는 경우
    • 403(Forbidden) : 접근이 금지될 경우
    • 404(Not Found) : 서버가 요청 받은 리소스가 없는 경우 (잘못된 URL로 요청을 하거나 리소스 자체가 없을 때)
  • 5xx(Server Error) : 서버가 유효한 요청을 수행하지 못하는 경우
    • 500(Internal Server Error) : 서버가 처리 방법을 모르는 경우
    • 502(Bad Gateway) : 게이트웨이나 프록시 작업 시 잘못된 응답을 수신한 경우
    • 503(Service Unavailable) : 서버가 요청을 처리할 준비가 되지 않은 경우 (서버에 과부하가 걸리거나 배포 도중 서비스가 중단되는 상황 등에 해당 상태 코드를 반환한다.)

메서드

클라이언트는 요청 메서드를 통해 웹 서버에 전송할 HTTP 요청의 목적, 종류를 나타낼 수 있다. 메서드는 총 9가지가 존재하며, 각 메서드는 데이터의 조회나 변경 등 목적에 맞춰 사용된다.

safe

서버의 상태를 변경하지 않는 메서드의 경우 ‘safe’하다고 한다. ‘읽기 전용’ 메서드라면 ‘safe’한 메서드이다. ‘safe’ 메서드를 호출하는 경우 서버에 변경 사항을 동반하지 않기 때문에 불필요한 부하를 주지 않는다. 대표적으로 GET, HEAD, OPTIONS 메서드는 ‘safe’ 하다고 볼 수 있다.

멱등성(Idempotent)

동일한 요청을 한 번 보낼 때와 여러 번 연속으로 보낼 때 같은 동작을 하고, 서버의 상태 또한 동일할 때 ‘멱등성(Idempotent)’을 갖는다고 한다. 모든 ‘safe’한 메서드는 ‘멱등성’ 또한 갖는다. 하지만 ‘멱등성’을 가진 메서드가 모두 ‘safe’한 메서드는 아니다. 대표적으로 GET, HEAD, PUT, DELETE 메서드가 ‘멱등성’을 가지며, 이 중 PUT과 DELETE는 서버의 상태를 변경하기 때문에 ‘멱등성’을 갖지만 ‘safe’하지 않은 메서드이다.

멱등성 ?
연산을 여러 번 적용하더라도 결과값이 달라지지 않는 성질

GET /tutorial HTTP 1.1
GET /tutorial HTTP 1.1
// 같은 응답을 받으므로 '멱등성'을 갖는다.

DELETE /delete/1 HTTP 1.1
// 존재 시 제거. 200 응답
DELETE /delete/1 HTTP 1.1
// 제거되었으므로 404. 상태 코드는 달라질 수 있지만 '멱등성'을 가진다.

POST /write HTTP 1.1 // 작성
POST /write HTTP 1.1 // 추가로 호출할수록 새로운 글이 작성되므로 '멱등성'을 갖지 않는다.

Cacheable

응답을 저장해 향후 재사용 할 수 있는 것을 의미한다.

  • GET : 특정 리소스를 가져올 때 사용한다.
  • POST : 서버로 데이터를 전송할 때 주로 사용한다. 이를 통해 서버에서 변경이 일어난다.
  • PUT : 요청 payload를 사용해 새로운 리소스를 생성하거나, 타깃을 payload로 대체한다. PUT 메서드는 POST 메서드와 다르게 멱등성을 갖는다.
  • PATCH : PUT 메서드는 payload로 리소스를 대체하지만, PATCH는 리소스의 특정 부분을 수정할 때 사용한다.
  • DELETE : 특정 리소스를 제거할 때 사용한다.
  • OPTIONS : 주어진 URL 또는 서버에 대해 허용된 통신 옵션을 확인할 때 사용한다. 서버에서 지원하는 메서드를 확인하거나 사전 요청(preflighted requests)을 보내는 용도로 사용한다.

헤더

HTTP 헤더는 서버와 클라이언트 사이에서 body 외에 추가적인 정보를 교환하는 방식이다. HTTP 메세지 제일 앞에 삽입되며 수십 개의 종류를 갖는다. 헤더는 대소문자를 구분하지 않고 이름:값 형태로 이뤄져 있다. 헤얻는 사용되는 문맥(context)을 기준으로 Entity 헤더, General 헤더, Request 헤더, Response 헤더로 분류된다.

Request와 Response 헤더

Request 헤더는 요청하는 자원이나 클라이언트 측의 정보를 포함한다. 대표적으로 클라이언트가 이해할 수 있는 정보를 나타내는 Accept-*나 요청에 조건을 부여하는 If-*가 있다. 또한 브라우저의 정보를 나타내는 User-Agent나 브라우저에 남아 있는 쿠키를 나타내는 Cookie가 요청에 포함될 수 있다.

반대로 Response 헤더는 서버의 위치나 응답에 대한 부가 정보를 포함한다. 또한 Request가 제공한 정보에 대한 답을 내려주기도 한다. 요청을 처리하는 서버를 나타내는 Server나 리다이렉트되어야 하는 위치를 나타내는 Location 등의 정보를 갖는다.

Entity 헤더

Entity 헤더는 본문에 대한 메타 정보 또는 본문이 없는 경우 요청으로 식별된 자원에 대한 메타 정보를 포함한다. 본문의 길이를 나타내는 Content-Length, text/html 같이 리소스 타입을 나타내는 Content-Type 등이 Entity 헤더 필드에 포함된다.

General 헤더

Request 및 Response 모두에서 사용되는 헤더로, 사용되는 문맥에 따라 Request 헤더가 되거나 Response 헤더가 된다. 전송되는 body의 값과는 직접적 관련이 없는 정보를 갖는다. General 헤더 필드에는 해당 통신이 만들어진 날짜를 의미하는 Date나 캐싱 메커니즘을 나타내는 Cache-Control 등이 포함된다. 상황에 따라 적절한 캐싱 헤더를 사용하여 서버와 클라이언트 간의 통신 시 성능을 높일 수 있다. 여기서 등장한 헤더 옵션을 포함해 40개 이상의 다양한 필드가 존재한다.

쿠키

HTTP 쿠키는 서버가 사용자의 웹 브라우저에 전송하는 작은 기록 정보 파일을 의미한다. HTTP 쿠키, 웹 쿠키, 브라우저 쿠키 등으로 불린다. 상태가 없는 (stateless) HTTP에서 브라우저는 쿠키 정보를 기록하여 동일한 서버에 재요청 시 저장된 쿠키 정보를 같이 전송한다. 과거에는 브라우저에 정보를 저장하는 유일한 방법이었기 때문에 쿠키를 사용하여 사용자 행동을 분석하거나 세션을 관리했다. 하지만 쿠키는 공개된 정보로 보안에 취약하여 개인 정보나 보안상 중요한 정보를 담기에는 적합하지 않다. 현재는 쿠키 외에 WebStorage(localStorage, sessionStorage) 같이 정보를 기록할 수 있는 저장소가 추가로 생겼기 때문에 사용성에 맞춰 선택하는 것을 권장한다.
쿠키는 이름, 값 속성을 갖는다. 이름-값 쌍으로 정보를 기록하며 쿠키의 만료 기간, 도메인, Secure, HttpOnly 등의 정보를 기록한다.

쿠키 만들기

서버에서 전송된 HTTP 응답의 Set-Cookie 헤더로 쿠키를 만든다.

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: cookie1=value1
Set-Cookie: cookie2=value2

// ...page content

응답을 통해 두 개의 쿠키 속성을 생성하여 저장한다.

GET /page.html HTTP1.1
Host: ...
Cookie: cookie1=value1; cookie2=value2

이후 서버로 전송되는 모든 요청에 해당 쿠키값을 회신한다.

속성

쿠키 삭제

쿠키는 세션이 종료될 때 제거된다. 하지만 Expires 속성에 날짜가 적혀 있거나 Max-age 속성에 기간이 명시되어 있다면 그 이후에 삭제된다. 두 속성이 모두 존재할 경우 Max-age가 우선 적용된다. Max-age의 단위는 초이다.

Set-Cookie: name=hanjung; Expires=Mon Oct 26 2020 12:00:00 GMT
Set-Cookie: name=hanjung; Max-age=3600

쿠키 스코프

Domain과 Path 속성은 쿠키에 접근할 수 있는 도메인과 URL 경로를 설정할 수 있다. Domain을 명시하지 않을 경우 현재 문서 위치를 기준으로 하며, 작성 시 서브 도메인을 포함한다. Domain=google.com이 될 경우 dev.google.com 같은 서브 도메인 또한 쿠키에 접근할 수 있다.
Path를 지정할 경우 해당되는 URL 경로와 서브 디렉토리 경로에서 쿠키에 접근할 수 있다. Path가 /docs인 경우, /docs/tutorial, /docs/tutorial/1 URL에서 쿠키에 접근할 수 있다. Path=/를 지정한다면 모든 페이지에서 쿠키에 접근할 수 있다.

Secure와 HttpOnly

Secure 속성을 사용할 경우 HTTPS로 통신하는 경우에만 쿠키가 전송된다. 기본적으로 쿠키는 프로토콜이 아닌 도메인만을 기준으로 판단한다. 하지만 Secure 속성을 부여하면 https에서 설정한 쿠키를 http에서 접근할 수 없다.
또한 HttpOnly는 클라이언트 측에서 쿠키를 사용할 수 없게 한다. document.cookie 같이 클라이언트 측에서 자바스크립트를 사용하여 쿠키에 접근하는 것을 막는다. 쿠키를 클라이언트에서 조작할 수 없도록 보호하고 싶은 경우 이 속성을 사용한다.
이 외에 samesite로 크로스 사이트 요청 위조(XSRF)를 막을 수 있는 속성 또한 존재하지만 IE나 버전이 낮은 브라우저에서 지원하지 않으므로 주의해서 사용해야 한다.

XSRF ?
웹 사이트의 취약점 공격 중 하나로, 사용자 모르게 해당 서비스에 특정 명령(공격자가 의도한 행위)을 요청하는 공격이다.

document.cookie

document.cookie 프로퍼티를 이용해 현재 접속한 사이트에 관련된 쿠키 정보를 알 수 있다. document.cookie는 이름=값 쌍으로 되어 있으며 ‘;’로 구분되어 있다.

document.cookie = "name=jung; path=/; expires=Mon Oct 26 2020 12:00:00 GMT";

쿠키 하나가 차지하는 용량은 최대 4KB이며, 브라우저에 따라 허용하는 쿠키는 약 20개 내외이다.

cookie를 사용하기 위해 document.cookie로 가져온 ‘;’와 ‘=’로 이루어진 문자열을 파싱해야 했다. 쿠키를 단일로 추가하거나 제거하는 것 또한 큰 불편함이 있었다. 이런 문제점을 해결하기 위해 크롬 87 버전부터 Cookie Store API가 새롭게 등장했다.

// cookie 가져오기
const cookie = await cookieStore.get("cookie-name");
// cookie 지정하기
await cookieStore.set({
  name: "cookie-name",
  value: true,
  domain: "example.com",
  expires: Date.now() + 3600,
});
// cookie 제거하기
await cookieStore.delete("cookie-name");

교차 출처 리소스 공유 (CORS)

교차 출처 리소스 공유(Cross Origin Resource Sharing, CORS)를 살펴보기 전 먼저 동일 출처 정책(Same Origin Policy, SOP)에 대해 알아볼 필요가 있다. 두 URL의 프로토콜, 포트, 호스트(Host)가 모두 같을 때 동일 출처라 부르게 된다.

예시 URL인 https://a.test.com:80/docs/1과 아래의 URL이 동일 출처인지 판단해보자.

  • https://a.test.com:80/tutorial
    • 프로토콜, 호스트, 포트가 모두 일치하므로 동일 출처 O
  • http://a.test.com:80/docs/1
    • 프로토콜이 다르므로 동일 출처 X
  • https://b.com:80/tutorial
    • 호스트가 다르므로 동일 출처 X
  • https://a.test.com:8080/tutorial
    • 포트가 다르므로 동일 출처 X

동일 출처 정책(SOP)은 한 출처로부터 로드된 문서나 자바스크립트가 다른 출처의 자원과 상호작용하는 것을 제한하는 보안 방식이다. 만약 ‘https://a.com/’에서 ‘https://api.a.com’ 출처의 API를 호출했다면, 두 URL의 출처가 서로 다르기 때문에 별도의 처리 없이 API 서버의 응답을 수신할 수 없다.

IE의 경우 두 가지 예외사항이 존재하는데, 양쪽 도메인이 높은 보안 수준을 가진 경우 동일 출처 정책을 적용하지 않는다. 또한 IE는 포트를 확인하지 않기 때문에 포트만 다를 경우 동일 출처로 본다. 하지만 이는 다른 브라우저에서 지원하지 않으며 표준이 아니다.

동일 출처 정책(SOP)은 브라우저의 보안을 위해 잠재적으로 위험이 있는 문서의 접근을 허용하지 않는다. 과거에는 웹 서버에서 요청을 처리해 결과를 정적인 페이지로 내려줬기 때문에 이런 정책이 크게 문제가 되지 않았다. 하지만 웹 사이트의 기능과 역할이 많아지면서 이런 정책이 문제가 되기 시작했다. 외부 서비스의 API를 활용하거나 클라이언트와 API 서버를 분리하여 웹 사이트를 만드는 경우가 많아졌기 때문이다.
이런 문제를 해결하기 위해 허용된 출처를 서버에 알려줄 수 있는 HTTP 헤더가 추가되었다. CORS는 이 HTTP 헤더를 이용해 애플리케이션에서 다른 출처의 리소스에 접근할 수 있도록 권한을 줄 수 있다.
CORS의 동작은 요청을 보낸 헤더의 Origin 필드와 서버가 보내준 Access-Control-Allow-Origin을 비교해 이 응답이 유효한지 브라우저에서 판단한다. 이때 유효하지 않을 경우 브라우저는 해당 응답을 에러 처리한다. 단순한 내용이지만 CORS가 동작하는 시나리오는 여러 가지가 있다. 여기서는 가장 일반적으로 많이 사용되는 사전 요청(preflighted) 방식을 다룬다.

사전 요청(preflighted) 방식은 OPTIONS HTTP 메서드를 이용해 실제 요청을 전송하기 전 안전한 요청인지 허가를 받은 뒤 실제 요청을 보내는 방식이다. 사전 요청은 일반적으로 브라우저에서 자동으로 전송하기 때문에 클라이언트 개발자가 직접 작성하는 경우는 없다.

const xhr = new XMLHttpRequest();

xhr.open("DELETE", "https://other-example.com/docs/1");
xhr.send();

예를 들어 DELETE 요청을 다른 도메인에 보낸다고 하면 이 요청은 사전 요청을 거쳐 실제 요청을 보내게 된다.
브라우저에서 실제 DELETE 요청을 보내기 전 사전 요청을 보내 해당 도메인의 DELETE 메서드를 허용하는지 물어본다. Access-Control-Allow-Origin은 서버에서 허용하는 출처가 무엇인지 알려주며, 브라우저는 이를 이용해 리소스에 접근할 수 있는지 판단한다.

OPTIONS /docs/1
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://www.example.com

만약 서버가 해당 요청을 허용할 경우 응답 헤더의 Access-Control-Allow-Methods 필드에 허용하는 메서드가 나타나며, Access-Control-Allow-Origin 필드에 허용하는 출처에 대해 나타난다.

HTTP/1.1 204 No Content
Connection: keep-alive
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE

사전 요청이 마무리되면 실제 DELETE 요청을 보낼 수 있게 되어 원래 요청을 보낸 뒤 응답을 받는다.

브라우저에서 제공되는 XMLHttpRequest나 fetch()는 출처가 다를 경우 브라우저의 쿠키나 인증 정보를 헤더에 담지 않는다. 만약 인증 정보를 요청에 담아 보내고 싶다면 credentials 옵션을 변경해야 한다. XMLHttpRequest의 경우 withCredentials 속성을 변경해야 한다. credentials는 총 3개의 값을 가질 수 있다.

  • same-origin : 동일 출처라면 보낸다. 이 값은 디폴트 값으로 사용된다.
  • omit : 동일 출처 여부와 상관없이 항상 쿠키를 전송하지 않는다.
  • include : 교차 출처라 할지라도 보낸다.

다음과 같이 요청의 credentials를 include로 설정한 경우 허용된 교차 출처에 인증 정보를 전송할 수 있다.

fetch(url, {
  credentials: "include",
});

하지만 한 가지 예외 상황으로, credentials이 include일 경우 서버에서 Access-Control-Allow-Origin을 와일드 카드(‘*‘)로 설정했다면 요청이 실패한다. 이는 명확하게 어떤 출처를 신뢰하는지 확인하기 위한 추가적인 안전 조치이다. 따라서 credentials: ‘include’를 사용할 경우 와일드 카드 대신 명확한 URL을 명시해야 한다.
해당 요청이 성공적으로 처리된다면 응답에는 Access-Control-Allow-Credentials가 true로 되어 반환되며, 해당 헤더가 포함되지 않는다면 응답은 무시된다.

JSON (JavaScript Object Notation)

JSON 포맷은 더글라스 크락포드(Douglas Crockford)가 처음 정의하고 보급한 문자 기반의 데이터 교환 형식이다. 자바스크립트 객체 문법에 일부 토대를 두고 만들어 JSON이라는 이름을 갖게 되었다. JSON은 언어로부터 독립적이지만, C, C++, C#, 자바, 자바스크립트, 파이썬 등을 사용하던 개발자들이 친숙하게 사용하던 관습을 그대로 적용했다. 대부분의 프로그래밍 언어는 표준에 맞게 JSON을 파싱하거나 직렬화(serialization)할 수 있다. JSON 포맷은 .json 확장자를 가진 텍스트 파일에 작성한다.

구조

JSON 구조는 자바스크립의 객체 리터럴 작성법을 따른다. 또한 자바스크립트의 원시 자료형인 문자열, 숫자, 불리언을 가질 수 있으며 중첩된 계층 구조를 가질 수 있다.

{
  "name": "giriboy",
  "age": 30,
  "male": true,
  "album": [
    {
      "name": "Sensual Album",
      "release": "January 20, 2014"
    },
    {
      "name": "Mechanical Album",
      "release": "May 31, 2016"
    }
  ]
}

자바스크립트의 객체 형태와 비슷하지만 완전히 일치하지 않는다. JSON은 순수한 데이터 포맷이기 때문에 메서드와 undefined, NaN, INFINITY 등과 같은 값은 존재하지 않는다. 또한 프로퍼티와 문자열 작성 시 작은따옴표(‘‘)는 사용이 불가능하며 큰따옴표(““)만 사용이 가능하다.

메서드

JSON은 문자열 형태이기 때문에 네트워크를 통해 전송할 때 매우 유리하다. 하지만 자바스크립트 객체를 데이터로 전송하거나 전송 받은 데이터를 사용할 경우 파싱이 필요하다. 이 경우 자바스크립트에서는 JSON 전역 객체의 stringify() 메서드를 이용해 문자열로 변환하거나 parse() 메서드를 이용해 JSON 문자열을 자바스크립트 객체로 변환할 수 있다.

stringify()

자바스크립트의 값이나 객체를 JSON 문자열로 변환한다. 네트워크를 통해 네이티브 객체를 문자열로 변환할 때 주로 사용한다. 변환 시 자바스크립트에 존재하는 타입이지만 JSON에 존재하지 않을 경우 몇 가지 규칙에 맞춰 변환된다.

  • Boolean, Number, String 타입은 기본형 값으로 변환된다.
  • 문자열화될 때 객체의 속성은 순서가 보장되지 않는다. (배열은 보장된다.)
  • undefined, Symbol 타입은 객체 안에 있을 시 생략된다. 배열 안에 존재할 시 null로 변환된다.
  • NaN, Infinity의 경우 null로 표현된다.
JSON.stringify({}); // "{}"
JSON.stringify(false); // "false"
JSON.stringify(100); // "100"
JSON.stringify([
  1,
  NaN,
  () => {
    console.log("hi");
  },
]); // "[1, null, null]"
JSON.stringify({
  a: 1,
  b: NaN,
  c: () => {
    console.log("hi");
  },
}); // "{"a":1, "b":null}"

parse()

JSON 텍스트 (JSON 형태의 문자열)를 구문 분석하고 ECMAScript 값을 생성한다. 구문이 분석되면 ECMAScript 객체로 표현된다. 타입이 일치하는 배열, 문자열, 숫자, 불리언, null은 ECMAScript의 타입으로 변환된다.

const json = '{"a": 100, "b": null, "c": true}';
const obj = JSON.parse(json);

JSON.parse(obj);
console.log(obj.a, obj.b, obj.c); // 100, null, true

Ajax

초기의 웹 페이지는 서버로 요청을 보내고 응답을 받아 전체 페이지를 다시 그리는 형태로 구현되었다. 항상 전체 페이지에 대한 응답을 받기 때문에 비슷한 형태의 페이지를 나타내거나 일부 데이터의 갱신만 필요한 경우에 많은 리소스가 낭비되었다. 하지만 2000년대 중반 구글이 웹 서버와 비동기적으로 통신하는 기술을 사용하면서 웹에 큰 변화가 시작된다. 기존과 달리 전체 페이지를 불러오는 것이 아닌 페이지의 일부분만을 갱신할 수 있어 응답성을 크게 향상시킬 수 있었다. 이후 이 기술은 W3C의 표준 명세에도 포함되었으며, 현재는 Ajax(Asynchronous JavaScript And XML)라는 이름으로 불리고 있다. 그리고 브라우저에서는 XMLHttpRequest라는 API로 Ajax를 사용할 수 있도록 제공한다.

Ajax를 사용하면 페이지 이동없이 빠르게 화면을 동적으로 변경할 수 있으며, 초기에 수신해야 하는 데이터의 양을 줄여 빠르게 로딩할 수 있다.

XMLHttpRequest

XMLHttpRequest는 브라우저와 웹 서버 간에 통신을 하여 리소스를 가져올 수 있는 API이다. 이름과 상관없이 XML 외 모든 종류의 데이터를 받아올 수 있으며 http 뿐만 아니라 ftp, file 같은 프로토콜 또한 사용할 수 있다. XMLHttpRequest는 옵션을 통해 비동기와 동기 방식 모두를 사용할 수 있다.

XMLHttpRequest로 Ajax 방식의 통신을 하기 위해서는 인스턴스를 생성한 후 open() 메서드를 통해 요청 환경을 구성해야 한다. open() 메서드는 필수 매개변수로 http 메서드, 요청할 url을 가진다. 선택 매개변수로는 요청이 동기인지 비동기인지 확인하는 async와 인증 시 필요한 username, password와 같은 매개변수들이 있다.

const xhr = new XMLHttpRequest();
const method = "POST"; // 'GET', 'DELETE' 등 여러 메서드 가능

const url = "/post";

xhr.open(method, url);

서버에 요청을 보낼 때는 send() 메서드를 사용한다. 매개변수로는 body가 있으며, POST 요청처럼 Request body에 데이터를 담아 전송할 때 사용한다. 또한 abort() 메서드를 통해 언제든지 요청을 중지할 수 있다.

const body = JSON.stringify({ userId: 100 });

xhr.send(body); // 요청 보냄
xhr.abort(); // 요청 중지

헤더 다루기

요청에 헤더를 지정하고 싶다면 setRequestHeader() 메서드에 이름과 값을 인자로 넘겨 원하는 헤더 필드를 지정한다. 단, setRequestHeader()를 사용하여 Cookie나 Host, Connection 등 보안상으로 문제가 될 수 있는 필드를 수정하는 것은 불가능하다. 해당 필드들은 덮어쓰는 형태가 아닌 새로운 값을 추가하는 형태로만 동작하며, 제거하는 것은 불가능하다.

xhr.setRequestHeader("X-Text", "han");
xhr.setRequestHeader("X-Text", "lee");

// X-text : han, lee

HTTP Post 메서드로 통신할 경우 전송하는 body 데이터의 미디어 타입은 Content-type 필드를 통해 표현한다. Content-type에 지정한 미디어 타입 (예: application/json, multipart/form-data)으로 body의 데이터 타입이 결정된다.

xhr.setRequestHeader("Content-Type", "application/json");

응답으로 온 헤더 정보가 필요한 경우 getResponseHeader()getAllResponseHeader() 메서드를 사용한다. getResponseHeader()는 원하는 헤더 필드의 이름을 인자로 넘겨 값을 가져온다. getAllResponseHeader()는 모든 헤더의 이름과 값을 개행으로 구분한 문자열로 반환한다.

xhr.getResponseHeader("Content-Type");
xhr.getAllResponseHeader();

이벤트

응답을 다루는 이벤트는 여러 가지가 존재한다. 요청이 시작될 때 발생하는 loadstart, 요청한 데이터를 받는 동안 주기적으로 발생하는 progress, 요청이 성공적으로 종료되었을 때 발생하는 load, 성공 여부 관계없이 요청 처리 과정이 완료되었을 때 발생하는 loadend가 있다. 그 외에 요청 처리에 문제가 발생했을 때 timeout, abort, error 이벤트로 각각 예외를 처리할 수 있다. error 이벤트는 HTTP 상태 코드가 4xx나 5xx일 때 발생하는 것이 아니라 네트워크 장애 요인으로 요청이 완료되지 못했을 경우 발생하니 유의해야 한다.

xhr.addEventListener("progress", () => {
  console.log();
});

xhr.addEventListener("load", () => {
  console.log(xhr.status); // 상태 코드
  console.log(xhr.response); // 응답 body
});

xhr.addEventListener("error", () => {
  console.log(error);
});

또한 readyState 프로퍼티를 통해 현재 요청 상태를 알 수 있다. 이 값은 객체의 상수와 숫자로 비교할 수 있다. 상태는 UNSENT, OPENED, HEADERS_RECEIVED, LOADING, DONE을 가지며 각각 차례대로 0부터 4까지의 숫자를 갖는다.

xhr.addEventListener("readystatechange", () => {
  if (xhr.readyState === xhr.OPENED) {
    // 상수로 readyState 비교하기
  } else if (xhr.readyState === 3) {
    // 숫자로 readyState가 LOADING 상태인지 비교
  }
});

Promise

pourWater(() => {
  bringToBoil() => {
    putRamenAndSoupBase(() => {
      setTimeout(() => {
        turnOffGasFire()
        eatRamen()
      }, 3*60*1000)
    })
  }
})

비동기로 코드를 작성하다 보면 예제처럼 중첩된 콜백 함수를 매개변수로 사용하는 경우가 발생한다. 이런 패턴을 콜백 지옥 (Callback Hell) 혹은 파멸의 피라미드 (Pyramid of Doom)라 부른다. 이런 구조는 코드의 들여쓰기가 깊어져 이해하기가 힘들고 유지 보수하기 힘든 코드이다.

ES2015에서 새롭게 도입된 Promise 객체를 이용하면 비동기 처리 시점을 명확하게 표현할 수 있다. Promise는 비동기 작업이 끝난 뒤 결괏값(성공 또는 실패)을 처리할 수 있는 객체이다. Promise 생성자 함수를 통해 인스턴스화하며, 생성자 함수에 전달되는 함수를 실행자(executor)라고 한다. 전달된 실행자는 인스턴스가 생성될 때 비동기 작업을 수행한다.

const promise = new Promise((resolve, reject) => {
  // 실행자(executor)
  // 비동기 작업 수행. 객체가 만들어질 때 실행된다.
});

실행자 함수는 매개변수로 자바스크립트에서 제공하는 함수인 resolve()reject()를 가진다. 비동기 작업이 성공적으로 실행되었을 경우 결괏값과 함께 resolve() 함수를 호출하며, 실패했을 경우 에러 객체와 함께 reject() 함수를 호출한다. 실행자에서는 reject()resolve() 중 반드시 하나를 호출해야 하며 한 실행자 내에서 reject()resolve()를 중복해서 호출하면 무시된다.

const promise = new Promise((resolve, reject) => {
  resolve(100);
  resolve(200); // 무시
});

또한 Promise 객체는 상태를 나타내는 state 프로퍼티를 갖는다. 총 세 개의 상태를 가질 수 있으며, 각 상태는 상호 배타적인 관계이다. 작업이 성공적으로 종료되었음을 의미하는 이행(fullfiled)과 작업이 실패했다는 것을 의미하는 거부(rejected), 그리고 작업이 이행되거나 거부되지 않은 초기 단계를 의미하는 보류(pending)가 있다.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const random = Math.floor(Math.random() * 10);

    if (random % 2) {
      resolve("홀수");
    } else {
      reject(new Error("짝수"));
    }
  }, 100);
});

예제 코드에서는 Promise 인스턴스가 생성될 때 실행자 함수가 호출되어, setTimeout() 함수로 타이머를 설정한다. 1초 후에 선언된 random 정수가 홀수인 경우 resolve() 함수가 호출되고 짝수인 경우 Error 객체를 만들어 reject() 함수가 호출된다.

then(), catch() 그리고 finally()

<참고 : https://mangkyu.tistory.com/15
https://ko.javascript.info/fetch-crossorigin >


About Hyerin Jeon

안녕하세요, 전혜린 (Hailey) 입니다! 주니어 프론트엔드 개발자이며, 리액트를 사용하고 있습니다 :D

-->
Useful Links