[15-1] API란?
[15-2] 웹페이지의 통신
[15-3] Promise 객체
[15-4] 구조 분해 할당
[15-5] 값의 복사
[15-6] 구조분해할당 적용
[15-7] JSON 데이터 활용
[15-8] 렌더링 딜레이 개선
API란, 어떠한 프로그램에서 제공하는 기능을 사용자가 활용할 수 있도록 만들어 둔 인터페이스다.
우리가 접근하는 사이트 혹은 프로그램을 어떠한 식당이라고 가정해 보겠다.
그 식당에서 팔고 있는 메뉴들을 보여주는 메뉴판
, 이것을 우리는 API 명세서
라고 부른다.
우리가 사용할 수 있는 기능
들이 무엇이 있는지, 혹은 어떻게 사용하는 것인지
가 적혀져 있다.
메뉴를 주문하는 행위, 해당 사이트, 프로그램에게 어떠한 기능을 사용하겠다 라고 전달하는 행위를 요청(Request)
이라고 한다. 이때 요청을 전달하는 사람을 클라이언트(Client)
라고 부른다.
이때 그 주문, 요청을 받아서 실제로 음식을 조리해주는 요리사에게 주문을 전달해주는 직원과 같은 역할.
요청을 받아 데이터를 가공해주는 서버에게 어떤 요청이 들어왔는지를 전달해주는 것
이 바로 API
다.
그리고 우리가 돌려 받게 되는 음식, 데이터를 응답(Response)
이라고 한다.
API와 같이 다른 서버와 통신을 할 때는 특정한 규칙에 맞춰 그 통신이 이루어져야 한다.
HTTP
는 간단하게 이야기해서 서로 다른 서버 간에 문자 형식으로 데이터를 주고 받을 때 지켜야 하는 규약
이다.
HTTP에서 요청을 보낼 때는 대상 서버로 HTTP 메세지를 보내고 요청 헤더(Request header)
와 요청 바디(Request body)
가 그 안에 담겨진다.
이미지 가장 윗줄의 GET /data/2.5/weather? HTTP/1.1
에 해당하는 부분은 start-line
이라 부른다.
우리가 사용한 HTTP Method, 요청 URI, HTTP의 버전
을 담고 있다.
그리고 그 아래의 Host부터가 요청 헤더
에 속한다.
Request header
에는 다양한 정보가 담기게 되는데, 요청을 받는 서버의 이름, 서버의 버전, 전달하는 컨텐츠의 타입, 요청 날짜, 요청을 보낸 컴퓨터
의 정보 등 수많은 내용이 담겨진다.
그리고 Request header의 내용이 모두 종료되면 하나의 빈 줄로 Request body와 구분해 준다.
Request body에는 우리가 서버
로 혹은 다른 사용자가 우리의 서버로 전달하고자 하는 컨텐츠
를 담게 된다.
이 때, 어떠한 메서드
를 통한 요청인지에 따라 Request body
를 담을 수 있는지, 없는지가 결정된다.
HTTP/1.1 200 OK에 해당하는 부분은 status-line
이라고 부릅니다. HTTP 버전과 상태 코드
(Status code), 응답 메세지
를 담고 있다.
그리고 두번째 줄부터가 Response header에 속한다.
Response header도 Request header와 마찬가지로 응답 날짜
, 응답을 전달한 버의 이름
, 서버의 버전
, 컨텐츠의 타입
등을 담고 있다.
Response header의 내용이 모두 종료되면 하나의 빈 줄로 Response body와 구분해 준다.
Response body에는 실제로 응답 리소스 데이터
가 담겨져 있다.
HTTP 요청은 각 요청마다 Method를 사용하며 그 역할이 모두 다르다.
우리의 운영체제에게 프로세스
라고 하는 작업 영역을 할당 받아 어떠한 동작을 수행한다.
코드의 실행
, 프로그램의 실행
, 파일 다운로드
등이 모두 프로세스별로 할당이 되고, 각 프로세스 내부에는 할당 받은 업무를 처리할 스레드
가 존재한다.
멀티 스레드
싱글 스레드
각각 자료구조 stack
과 queue
의 형태를 띄고 있다.
예를 들어, 위와 같은 코드를 실행하면, setTimeout
함수는 지연 시간을 지정해주지 않았음에도 불구하고 비동기 함수
이기 때문에 callback queue 영역으로 옮겨진다.
call stack 영역에 쌓인 함수는 LIFO 방식으로 수행되기 때문에 func3, func2, func1 순서로 함수가 완료된다.
그리고 해당 함수들이 모두 완료되면 그 후에 callback queue에 담긴 setTimeout 함수가 call stack으로 옮겨져 실행되며 마지막으로 "setTimeout call" 메세지가 출력된다.
한계점을 해결
, 비동기 작업을 수행하기 위해 가장 먼저 나온 해결책은 callback 함수
를 활용하는 것이었다.콜백 지옥(callback hell)
을 만나게 되는 문제가 있다.Promise
는 현재는 얻을 수 없지만 추후 작업이 완료되면 받아올 수 있는 데이터에 대한 접근 수단의 역할을 해준다. new Promise()
와 같은 코드를 입력하여 직접 생성해 줄 수 있다.
resolver
가 호출되고 비동기 처리에 실패하면(Rejected), reject
가 호출된다.Pending
상태임을 돌려받게 된다.통신을 통해 응답을 받아오는 과정은 코드가 실행되는 과정보다 느린 경우가 대다수다.
그렇기 때문에 통신 또한 비동기 처리
가 필요하다.
비동기 통신을 처리해주는 방법은 여러가지가 있는데, then()
, catch()
메서드를 사용해서 처리하는 방법이 있다.
fetch()
를 사용해주면 JavaScript에서 바로 통신을 수행할 수 있습니다.
const communicationResult = fetch(HTTP Request) console.log(communicationResult)
하지만 위와 같이 작성하면 정상적인 응답을 받지 못한다는 것을 확인했다.
그래서 응답이 돌아올 때 까지 기다려 주어야 하는데, 이때 then()
메서드를 사용할 수 있다.
fetch(HTTP Request).then()
fetch()
를 사용한 후 응답을 받아올 때 까지 기다리도록 fetch()
에 붙여서 then()
을 사용하면 된다.
then()
메서드는 fetch()
뿐만 아니라 Promise 객체
를 돌려주는 함수라면 언제든 사용할 수 있다.
fetch(HTTP Request) .then((res) => { console.log(res) })
then()
메서드 내부에 익명 함수를 하나 만들고 그 안에서 매개변수
로 데이터를 받을 수 있는데,
이때, 받아오는 데이터가 바로 통신의 결과로 우리에게 돌려준 데이터다.
만약 통신에서 문제가 생기거나 then()
메서드 내에서 로직을 수행하다가 에러를 만난다면 우리는 catch()
메서드로 그 분기를 나눠줄 수 있다.
fetch(HTTP Request) .then((res) => { console.log(res) }) .catch((err) => { console.error(err) })
catch()
메서드 내부에도 익명 함수를 넣어줄 수 있는데, 해당 함수의 매개변수로 error
의 내용을 받아올 수도 있습니다.
구조 분해 할당이란, 구조화 되어 있는 배열
, 객체
와 같은 데이터를 분해하여 각각의 변수에 다시 할당하는 것을 이야기한다.
값을 복사해주고 싶다면 어떻게 할 수 있을까?
- 먼저 선언 키워드를 적은 후
- 변수명이 위치하는 자리에 변수명을 모아 둔 배열을 넣고
- 데이터가 담긴 배열을 할당하면 된다.
const arr = [ 1, 2, 3, 4, 5 ]; let [ one, two ] = arr; console.log(one); // 1 console.log(two); // 2
- 선언 키워드를 먼저 적어주고
- 대괄호가 아닌 중괄호를 입력해서 구조분해할당을 수행합니다.
- 배열과 같이 중괄호 안에 담기는 명칭들이 변수명이 된다.
- 뒤에는 할당 연산자를 적어주신 후에 구조분해할당 해주고자 하는 객체를 적어주시면 된다.
const obj = { name: "otter", gender: "male" }
코드를 입력하세요
let { name, gender } = obj //let { name: a, gender: b } = obj
만약 배열 자체를 복사하고 싶다면 어떻게 해야할까?
const arr = [ 1, 2, 3, 4, 5 ]; const newArr = arr
이렇게 할 경우 arr
과 newArr
이 같은 주소값을 공유하기 때문에 복사라고 이야기하기엔 어려운 부분이 있다.
그렇기 때문에 복사를 위해서 사용해줄 수 있는 연산자가 존재한다.
const arr = [ 1, 2, 3, 4, 5 ]; console.log(...arr) // 1, 2, 3, 4, 5
spread 연산자를 사용하면 값의 복사를 수행할 수도 있다.
const arr = [ 1, 2, 3, 4, 5 ]; const newArr = [ ...arr ];
이렇게 하면 기존에 arr 배열이 가지고 있던 주소값과 전혀 다른 별개의 새로운 배열이 newArr이라는 변수에 담기게 된다. (참조 타입 값의 복사 성공)
heap
이라는 임시 저장 메모리에 담기게 된다.
이 heap이라는 영역은 참조 타입 데이터와 같이 그 데이터의 크기
가 유동적으로 변할 수 있다는 특징을 가지고 있다.
스프레드 연산자를 사용해서 객체의 중괄호를 한번 벗겨냈을 때 다른 property들은 모두 주소의 연결을 끊고 새 주소를 가진 데이터로 완전히 복사
가 되었지만
이 때 중첩된 favoriteFood
객체는 펼쳐지지 못하여 원본 객체의 favoriteFood와 복사된 객체의 favoriteFood는 여전히 같은 주소를 공유
하고 있다.
이러한 문제를 해결하기 위해 JSON.stringify()
와 JSON.parse()
를 사용할 수 있다.
Javascript Object Notation은 JS 객체 문법을 이용해 구조화된 데이터를 표현하기 위한 문자열 기반의 표준 포맷
const obj = { name: "otter", gender: "male", favoriteFood: { first: "sushi", second: "hamburger" } } const copy = JSON.stringify(obj) console.log(copy) // {"name":"otter","gender":"male","favoriteFood":{"first":"sushi","second":"hamburger"}}
위의 예시처럼 문자열로 변환이 된다면, copy에 담긴 값은 객체가 아닌 JSON이기 때문에 완전히 새로운 주소를 가지게 된다.
이후, JSON.parse()
를 사용해서 JSON 데이터 포맷
을 다시 객체 형태로 바꿔주면
const deepCopy = JSON.parse(copy) console.log(deepCopy) /* { name: "otter", gender: "male", favoriteFood: { first: "sushi", second: "hamburger" } } */
deepCopy
에 담기는 것이기 때문에 원본 객체와는 전혀 다른 주소를 가진 객체가 생성되는 것입니다.
이러한 복사를 깊은 복사
라 부릅니다.
const todoInput = document.querySelector('#todo-input');
const todoList = document.querySelector('#todo-list');
const savedWeatherData = JSON.parse(localStorage.getItem('saved-weather'));
const savedTodoList = JSON.parse(localStorage.getItem('saved-items'));
const createTodo = function (storageDate) {
let todoContents = todoInput.value;
if (storageDate) {
todoContents = storageDate.contents;
}
const newLi = document.createElement('li');
const newSpan = document.createElement('span');
const newBtn = document.createElement('button');
newBtn.addEventListener('click', () => {
newLi.classList.toggle('complete');
saveItemsFn();
})
newLi.addEventListener('dblclick', () => {
newLi.remove();
saveItemsFn();
})
if (storageDate?.complete === true) {
newLi.classList.add('complete')
}
newSpan.textContent = todoContents;
newLi.appendChild(newBtn);
newLi.appendChild(newSpan);
todoList.appendChild(newLi);
todoInput.value = '';
saveItemsFn();
}
const keyCodeCheck = function () {
if (window.event.keyCode === 13 && todoInput.value.trim() !== '') {
createTodo();
}
}
const deleteAll = function () {
const liList = document.querySelectorAll('li');
for (let i = 0; i < liList.length; i++) {
liList[i].remove()
}
saveItemsFn();
}
const saveItemsFn = function () {
const saveItems = [];
for (let i = 0; i < todoList.children.length; i++) {
const todoObj = {
contents: todoList.children[i].querySelector('span').textContent,
complete: todoList.children[i].classList.contains('complete')
};
saveItems.push(todoObj);
}
saveItems.length === 0 ? localStorage.removeItem('saved-items') : localStorage.setItem('saved-items', JSON.stringify(saveItems));
};
if (savedTodoList) {
for (let i = 0; i < savedTodoList.length; i++) {
createTodo(savedTodoList[i])
}
}
const weatherDataActive = function ({ location, weather }) {
const weatherMainList = [
'Clear',
'Clouds',
'Drizzle',
'Rain',
'Snow',
'Thunderstorm'
];
weather = weatherMainList.includes(weather) ? weather : 'Fog';
const locationNameTag = document.querySelector('#location-name-tag')
locationNameTag.textContent = location;
console.log(weather)
document.body.style.backgroundImage = `url('./images/${weather}.jpg')`
};
const weatherSearch = function ({ latitude, longitude }) {
const openWeatherRes = fetch(
`https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=33d9d8b3fcd26e46f20dad0561ec7a81`
).then((res) => {
return res.json();
}).then((json) => {
const weatherData = {
location: json.name,
weather: json.weather[0].main
};
weatherDataActive(weatherData);
}).catch((err) => {
console.log(err)
})
};
const accessToGeo = function ({ coords }) {
const { latitude, longitude } = coords;
// shorthand property
const positionObj = {
latitude,
longitude
}
weatherSearch(positionObj);
}
const askForLocation = function () {
navigator.geolocation.getCurrentPosition(accessToGeo, (err) => {
console.log(err);
});
}
askForLocation();
if (savedWeatherData) {
weatherDataActive(savedWeatherData);
}
// const promiseTest = function () {
// return new Promise((resolver, reject) => {
// setTimeout(() => {
// resolver(100);
// // reject('error');
// }, 2000);
// });
// };
// promiseTest().then((res) => {
// console.log(res);
// })