웹에서의 클라이언트 - 서버 통신은 기본적으로 HTTP 프로토콜 위에서 이루어진다.
[Hypertext]
웹 브라우저 상에서 쓰이는 HyperLink 이외에도 문서, 이미지와 같은 리소스를 말한다.
[Hyperlink]
웹 페이지나 문서에서 다른 웹페에지나 문서로 이동할 수 있는 링크
웹에서 데이터를 주고받기 위한 규약이다.
요청과 응답
클라이언트가 서버에 요청을 보내고, 서버는 그에 대한 응답을 반환하는 형식으로 통신이 이루어진다.
각 메세지는 헤더와 바디로 구성되며,
헤더에는 요청 혹은 응답에 대한 정보가 포함되고 바디에는 실제 데이터가 들어간다.
헤더
인증, 캐싱, 쿠키 등 다양한 기능을 지원한다.
무상태 Stateless
서버는 클라이언트의 상태를 기억하지 못하며, 원하는 데이터가 있다면 그 때마다 다시 요청을 보내야 한다.
비연결성 Connectionless
클라이언트와 서버는 한 번의 연결로 요청과 응답을 진행하고, 응답을 반환할시 바로 연결을 해제한다.
상태 코드
서버는 응답으로 상태코드를 반환해 클라이언트 요청에 대한 결과를 알려준다.
200은 성공, 401은 unAuthorized, 403은 Forbidden, 404는 Not Found, 300번대는 Redirect를 의미한다.
메소드
GET, POST, PUT, DELETE 등의 메소드를 통해 클라이언트가 서버에게 원하는 동작을 알릴 수 있다.
URL
URL을 사용해 원하는 리소스의 위치를 지정한다.
프로토콜, 도메인, 포트 번호로 이루어져 있다.
기본적으로 HTTP 프로토콜은 요청-응답이 끝나고 나면 연결이 끊기는 비연결성이 특징이다.
따라서 화면의 내용을 업데이트하기 위해 다시 요청-응답 과정을 거쳐 전체 페이지를 새로 그리게 된다.
그러나 업데이트가 필요 없는 부분도 다시 로드되어 엄청난 자원과 시간을 낭비하게 되는데, 이를 해결하기 위해 AJAX를 사용한다.
브라우저 API에서 제공하는 XMLHttpReqeust 객체를 이용해 비동기적으로 요청을 날리고 응답을 처리하는 기술이다.
이를 통해 웹페이지를 다시 로드하지 않고 동적으로 데이터를 업데이트 할 수 있다.
HTML과 같은 마크업 언어 중 하나로 태그를 이용해 데이터를 나타낸다.
통신 과정에서 XML을 사용하면 불필요한 태그로 인해 파일 사이즈가 커질뿐만 아니라, 가독성도 좋지 않아 최근 웹통신에는 JSON을 이용한다.
자바스크립트 객체 형식 key:value에서 영감을 받아 만들어진 데이터 타입이다.
장점
1. UX 개선
2. 서버 부하 감소 : 필요한 데이터만 부분적으로 재요청
단점
1. 검색 엔진 최적화 어려움 : 동적으로 생성되는 내용을 크롤링할 수 없음
2. 코드 복잡성 : 콜백 함수 중첩 등에 주의해야 함
다른 함수의 인자로 전달되는 함수를 말한다.
비동기 통신의 결과를 이용해 특정 작업을 수행해주기 위해서는 전통적으로 콜백 함수를 사용했다. 결과를 얻은 이후 실행할 함수를 계속해서 인자로 넘겨주는 동작 방식이다.
그런데 반환된 결과를 가지고 처리해야할 작업이 늘어난다면, 여기에 에러 처리까지 해줘야 한다면 말도 안되게 코드가 복잡해지며 분명 가독성도 떨어질 것이다.
이를 해결하기 위해 ES6에서 Promise가 도입되었다.
비동기를 간편하게 처리할 수 있도록 ES6이후 JavaScript에서 제공하는 객체다.
작업 수행이 성공적으로 끝나면 성공 메세지와 응답 데이터를 반환하고, 오류가 발생했다면 에러를 반환한다.
Promise 객체가 담고 있는 정보는 다음과 같다.
1. 작업의 성공 실패 결과
2. 성공 실패의 결과 값(데이터)
Promise 클래스로 Promise 객체를 생성한다.
executor라는 함수를 콜백함수로 전달하고, executor는 다시 resolve와 reject 두 가지 콜백함수를 인자로 받는다.
// Promise 객체의 executor는 선언 되자마자 바로 실행된다.
const promise = new Promise(function(resolve, reject) {
console.log('바로 실행 !')
})
// Arrow function으로 표현했을 경우
const promise = new Promise((resolve, reject) => {
console.log('바로 실행 !')
})
Promise의 상태는 세 가지가 있다.
pending
작업중
fulfilled
작업 성공 & 완료
reject
작업 실패
resolve
는 작업이 정상적으로 수행되었을 경우 호출되어 최종 데이터를 전달하는 함수다.const promise = new Promise((resolve, reject) => {
// network, 파일 읽기 등 오래 걸리는 작업 수행
console.log('바로 실행 !')
setTimeout(() => {
resolve('SUCCESS') // SUCCESS라는 값을 전달
}, 2000)
})
then
은 promise가 정상적으로 잘 수행되어 최종적으로 resolve 함수를 통해 전달한 res의 값('SUCCESS')에 접근할 수 있다.promise
.then((res) => {
console.log(res) // SUCCESS
})
reject
작업 중 문제가 발생했을 경우 호출하게 될 함수다. Error
라는 객체를 통해서 값을 전달한다. const promise = new Promise((resolve, reject) => {
// network, 파일 읽기 등 오래 걸리는 작업 수행
console.log('바로 실행 !')
setTimeout(() => {
reject(new Error('No network connection')) // SUCCESS라는 값을 전달
}, 2000)
})
catch
함수를 통해 reject 함수가 보내는 에러에 대한 정보를 얻을 수 있다.promise
.then((res) => {
console.log(res)
})
// then은 똑같은 Promise 객체를 반환하기 때문에
// chaining을 통해 catch로 접근할 수 있다.
.catch((error) => console.error(error))
finally
는 작업의 성공 실패와 관계 없이 무조건 호출되는 함수다.promise
.then((res) => {
console.log(res)
})
.catch((error) => console.error(error))
.finally(() => console.log('finally'))
브라우저에서는 비동기 통신을 처리하기 위해 fetch()
내장 함수를 제공한다.
브라우저에서 전역적으로 동작하기 때문에 그냥 호출해서 사용하면 된다.
브라우저 DOM Tree의 최상위 객체인 window를 통해 window.fetch()
로도 사용이 가능하다.
// 첫 번째 인자로는 url, 두 번째 인자로는 options 객체를 받는다.
// 요청에 대한 응답으로 Promise 객체를 반환한다.
fetch(url, options)
.then((res) => console.log(res))
.catch((err) => console.log(err));
response 객체의 JSON 형식의 응답 데이터를 자바스크립트 객체로 변환해 얻을 수 있다.
우리가 흔히 사용하는 axios 라이브러리는 이 작업을 자동으로 진행한다.
fetch(url, options)
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => console.log(err))
비동기 처리에서 Promise 체이닝이 너무 길어진다 싶을 때, 혹은 코드 구조상 적절한 곳에 사용할 수 있다.
함수 앞에 async
키워드만 붙여주면 알아서 Promise 객체를 반환한다.
// 다음의 두 함수는 똑같이 동작한다.
function fetchYeji(){
return new Promise((resolve, reject) => {
resolve('Yeji')
})
}
async function fetchYeji() {
return 'Yeji'
}
fetchYeji(res).then(console.log(res)) // Yeji
// 아무런 인자도 없으면 then에서 받아오는 값을 바로 전달 받는다.
fetchYeji().then(console.log) // Yeji
function delay(ms) {
// ms 후에 resolve 함수 호출
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function getApple(){
await delay(1000) // 1초 후에 resolve 호출, 사과 반환
return '🍎'
}
async function getBanana(){
await delay(1000)
return '🍌'
}
사과와 바나나를 따서 결과를 출력해보자.
사과를 땄다면 => 바나나를 따러 간다 => 바나나를 땄다면 => 사과와 바나나를 반환한다
만약 이렇게 계속해서 다른 과일을 차례로 따야 한다면 콜백 지옥과 똑같이 복잡한 구조가 될 것이다.
function pickFruits() {
return getApple().then((apple) => {
return getBanana().then((banana) => `${apple} + ${banana}`)
})
}
pickFruits().then(console.log) // 🍎 + 🍌
이를 async와 await을 통해 다음과 같이 간단하게 만들 수 있다.
async function pickFruits() {
const apple = await getApple()
const banana = await getBanana()
return `${apple} + ${banana}`
}
pickFruits().then(console.log) // 🍎 + 🍌
에러 처리를 해주고 싶다면 다음과 같이 할 수 있다.
async function pickFruits() {
try {
const apple = await getApple()
const banana = await getBanana()
return `${apple} + ${banana}`
} catch (err) {
console.log(err)
}
}
그런데 getApple과 getBanana 함수는 둘 사이에 아무런 연관이 없다.
즉, 위와 같이 작성하게 되면 then 체이닝으로 인해 동기적으로 실행돼 사과와 바나나를 따는데 2초가 걸릴 것이다.
두 함수를 동시에 실행시키기 위해서 선언되자마자 executor 함수를 실행시키는 Promise의 특징을 이용할 수 있겠다.
async function pickFruits() {
const applePromise = getApple()
const bananPromise = getBanana()
const apple = await applePromise
const banana = await bananPromise
return `${apple} + ${banana}`
}
그런데 이 방법도 뭔가 지저분하다. 이와 같은 상황에서 사용할 수 있는 것이 바로 Promise API다.
Promise 배열을 전달하면 모든 Promise를 병렬적으로 실행시켜 결과를 반환한다.
function pickAllFruits() {
return Promise.all([getApple(), getBanana()]).then((fruits) =>
fruits.join(' + ')
)
}
pickAllFruits().then(console.log)
만약 먼저 완료된 결과만을 반환하고 싶다면 race라는 API를 이용하면 된다.
async function getApple(){
await delay(2000) // 2초 후에 resolve 호출, 사과 반환
return '🍎'
}
async function getBanana(){
await delay(1000) // 1초 후에 resolve 호출, 바나나 반환
return '🍌'
}
그러면 1초 후에 resolve가 호출되는 바나나가 결과로 들어가 호출될 것이다.
function pickOnlyOne() {
return Promise.race([getApple(), getBanana()])
}
pickOnlyOne().then(console.log) // 🍌