Javascript는 싱글 스레드
언어이다.
싱글 스레드
와 멀티 스레드
가 어떻게 다른지 이미지로 알아보자.
스레드
는 프로세스가 할당 받은 자원을 이용하는 실행의 단위이다.
멜론에서 노래를 들으면서 + 워드에 작업을 하면서 + 카카오톡을 이용할 수 있다.
이때 각각의 응용프로그램(멜론, 워드, 카카오톡)은 하나의 프로세스
를 갖는다.
멜론도 하나의 프로세스, 워드도 하나의 프로세스인 것이다.
스레드
는 프로세스를 여러개로 나눈 조각이라고 할 수 있다.
워드를 이용하는 경우 사용자가 텍스트를 입력하는 동안 주기적으로 자동 저장을 하고, 입력한 내용을 화면에 출력하고, 입력하는 동안 맞춤법 검사를 실시한다.
이 모든 작업들은 각각의 스레드에 의해 이루어진다.
글자를 입력 받는 스레드, 자동 저장을 하는 스레드, 화면에 출력하는 스레드, 맞춤법 검사를 하는 스레드가 각각 존재하는 것이다.
즉, 워드라는 큰 프로세스
안에 스레드
가 모여있는 것이다.
이를 토대로 다시 이해를 해보면, 멀티 스레드
는 한 프로세스에서 여러 스레드를 번갈아 가며 작업을 처리하는 방식이고, 싱글 스레드
는 한 프로세스에서 단일 스레드만으로 작업을 처리하는 방식이다.
💡 Process vs Core vs Thread
1.Process
- 컴퓨터 운영을 위해 기본적인 명령어에 반응하고 처리하는 논리회로로, 보통은 CPU와 같은 의미로 쓰인다.
core
- CPU의 각종 연산을 하는 핵심 부품이다. (프로세스 내에 코어가 있는 것!)
Thread
- Process와 Core가 하드웨어적 관점이라면, Thread는 소프트웨어적 관점에서 프로세스 내에서 실제로 작업을 수행하는 주체를 의미한다.
멀티 스레드
context-switching
을 위한 오버헤드가 작아서 메모리 리소스가 상대적으로 적다. 💡 Context-switching
한 프로세스에서 다른 프로세스로 CPU 제어권을 넘겨주는 작업이다.
한 Thread의 작업을 진행하기 위해 해당 Thread 의 Context(process가 현재 어떤 상태로 수행되고 있는지에 대한 정보)를 읽어오고,
다시 다른 스레드로 작업을 변경할 때 이전 스레드의 Context를 저장하고, 작업을 진행할 스레드의 Context를 읽어오는 식으로 동작한다.
이 시간이 길어진다면 프로그램의 성능 저하가 발생하고 프로그램이 멈추기도 한다.
같은 프로세스 내에서 하나의 스레드에서 다른 스레드로 제어권을 넘겨주는 작업은 Thread Context Switching이라고 한다.
멀티 스레드의 경우 자신이 속한 프로세스의 자원들을 공유하기 때문에 Thread Context Switching이 더욱 경제적이다.
동시성 이슈
가 있다. 💡 동시성 이슈란?
멀티 스레드
방식을 이용할 때 여러 스레드가 자원을 공유하기 때문에 같은 자원을 두고 경쟁상태가 되는 문제를 의미한다.
여기서병렬성
과 동시성을 혼돈하면 안되는데
다음과 같이
그전까지 웹 사이트를 구현하는 개발자에게 멀티 스레드 언어인 자바는 다소 무겁고 어려운 언어였다.
이에 따라 자바스크립트는 원래 웹페이지의 보조적인 기능을 수행하기 위해 만들어졌다.
자바는 멀티 스레드 언어로서 동시성 이슈
가 있었기에 복잡하지 않은 싱글스레드 형식이 채택된 것이다.
그런데 우리는 하나의 프로세스인 브라우저에서 유튜브로 노래를 들으면서 포털창에서 검색을 하기도 한다.
어떻게 싱글 스레드
언어인 Javacript를 Multi Thread
로 이용하고 있을까?
동기
: 요청을 보낸 후 응답을 받아야만 다음 동작이 이루어진다. 호출한 함수가 작업 완료를 신경 쓴다.비동기
: 요청을 보낸 후, 결과를 기다리지 않고 다른 작업을 처리한다. 호출된 함수(callback 함수)가 작업 완료를 신경 쓴다.💡 비동기 vs 멀티스레드
쓰레드는 공간을, 동기/비동기는 순서의 여부를 알려주기에 둘은 의미가 다르다.
싱글 스레드 + 동기 / 싱글스레드 + 비동기 / 멀티스레드 + 동기 / 멀티스레드 + 비동기 의 모든 경우가 가능하다.
참고자료
자바스크립트는 싱글 스레드
언어이지만 자바스크립트 런타임의 Web API, Event Loop으로 인해 멀티스레드처럼 여러 일을 한 번에 수행하는 것처럼 보인다.
이를 이해하기 위해 필요한 개념을 정리해보자
런타임이란 프로그래밍 언어가 구동되는 환경을 말한다.
자바스크립트 런타임은 Javascript engine, Web API, Task Queue, Event Loop, Render Queue로 구성되어있다.
Javascript engine은 Memory Heap
과 Call Stack
으로 이루어져있다.
브라우저에서 자바스크립트를 비동기적으로 사용할 수 있도록 제공하는 여러 도구들로, XMLHttpRequest / DOM Events / setTimeout, setInterval / setState 등이 있다.
1️⃣ Call Stack
에서 호출된 함수는 2️⃣ Web API
를 호출하고, 2️⃣ Web API
는 콜백함수를 3️⃣ Task Queue
에 넣는다.
4️⃣ Event Loop
는 1️⃣ Call Stack
과 3️⃣ Task Queue
를 체크하고 1️⃣ Call Stack
이 비어있으면 3️⃣ Task Queue
의 첫번째 콜백함수를 1️⃣ Call Stack
에 push한다.
console.log('hello');
setTimeout(() => {
console.log('ing');
}, 3000);
console.log('bye');
console.log('hello')
가 call Stack에 들어가고 바로 실행되어 pop된다. setTimeout
이 호출되어 call Stack에 들어간 후 바로 pop되어 Web API로 호출된다. setTimeout
의 타이머가 돌아가는 동안 console.log('bye')
가 call Stack에 들어가고 바로 실행되어 pop된다. setTimeout
은 콜백 함수(() => { console.log('ing') })를 Task Queue
에 넣는다. Event Loop
은 Call stack
이 비어있는 것을 확인하고 콜백함수를 call Stack에 push하고 이는 바로 실행되어 pop된다. 💡 정리
1. call stack에 호출된 함수가 들어간다.
2. 그 중 특정 작업 (ex. setTimeOut)을 Web API로 보내고 특정 시간이 되면 콜백함수를 Task Queue로 보낸다. 그 시간 동안 Call stack에는 다른 작업이 계속 들어오고 나갈 수 있다.
3. Event Loop가 Call Stack이 비는 순간을 포착해서 Task Queue의 콜백함수를 call stack에 push하여 콜백함수가 실행된다.
💡 그래서 비동기 처리 가 뭔데?!
비동기가 특정작업이 끝날 때까지 다른 작업을 기다리게 하는 것이 아니라 나머지 작업을 먼저 수행하는 것이라는 것은 알았다.
한 번에 두 가지 이상의 작업을 동시에 처리하는 것처럼 보인다는 것도 알았다.
그런데 내가 고민했던 것은 그러면 비동기 처리는 결국 실행 순서를 뒤죽박죽으로 만들도록 처리한다는 것인가?에 대한 의문이었다.
오랜 고민 끝에, 그리고 이 글을 참고하여 다음과 같이 이해할 수 있었다.
자바스크립트의 비동기의 동작 방식은 빠르게 처리할 수 있는 것부터 처리를 하는 것인데, 이는 컴퓨터의 동작에서 매우 중요하다.
파일 2개를 다운받을 때 먼저 다운로드가 완료될 수 있는 것을 끝내는 게 맞으니까.
우리가 콜백이나 fetch, async await을 써서 굳이 그 동작 순서를 뭐가 완료되고 무조건 뭐가 실행되게!
이렇게 제어하는 이유는 비동기 방식으로 동작하는 코드 (ex.setTimeout)를 사용하며 실행순서가 중요한 경우 (ex. 서버통신으로 데이터를 받아오고 그 데이터를 변수에 넣어서 리턴해야하는 경우) 그 순서를 제어하기 위해서 사용한다.
비동기로 동작하는 코드의 실행 순서를 제어하기 위해 사용하는 비동기 처리 방법에는 3가지가 있다.
콜백은 매개변수로 함수를 전달해서 호출 함수 내에서 함수를 실행하는 것을 의미한다.
콜백함수를 사용하지 않았을 때의 예시를 보자.
function greet() {
console.log('안');
setTimeout(() => console.log('녕'), 400);
console.log('하세요');
}
// ✅ setTimeout이 비동기로 작동하기에 안-하세요-녕 순서로 출력된다.
greet( );
그러면 이제 콜백함수를 사용해서 실행순서를 제어해 안녕하세요
를 출력해보자.
function greet() {
setTimeout(() => {
console.log('안');
setTimeout(() => {
console.log('녕');
setTimeout(() => {
console.log(하세요');
}, 30);
}, 30);
}, 30);
}
greet();
위 코드는 다음과 같이 동작한다.
greet 함수가 콜 스택에 push되어 실행된다.
첫번째 setTimeout 함수가 콜스택에 들어갔다가 바로 Web API로 이동한다. 그리고 30/1000초의 시간 동안 대기한다.
대기를 마친 후 첫번째 setTimeout 함수의 콜백함수가 task Queue로 이동한다. 콜스택이 비어있으므로 바로 콜스택으로 이동하여 함수를 실행한다. 이로 인해 안
이 출력된다.
3번의 콜백함수 내의 setTimeout이 다시 바로 Web API로 이동한다. 그리고 30/1000초의 시간 동안 대기한다.
대기를 마친 후 두번째 setTimeout 함수의 콜백함수가 task Queue로 이동한다. 콜스택이 비어있으므로 바로 콜스택으로 이동하여 함수를 실행한다. 이로 인해 녕
이 출력된다.
5번의 콜백함수 내의 setTimeout이 다시 바로 Web API로 이동한다. 그리고 30/1000초의 시간 동안 대기한다.
대기를 마친 후 setTimeout 함수의 콜백함수가 task Queue로 이동한다. 콜스택이 비어있으므로 바로 콜스택으로 이동하여 함수를 실행한다. 이로 인해 하세요
가 출력된다.
이와 같은 동작으로 실행순서를 제어했지만 이 방법에는 치명적인 단점이 존재한다.
콜백 함수를 익명 함수로 전달하는 과정에서 또 다시 콜백 안에 함수 호출이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상을 말한다.
step1(function (value1) {
step2(function (value2) {
step3(function (value3) {
step4(function (value4) {
step5(function (value5) {
step6(function (value6) {
// Do something with value6
});
});
});
});
});
});
보기만 해도 어지럽고, 가독성도 떨어진다.
이에 대한 대안으로 나온 것이 Promise
와 async-await
이다.
Promise
는 자바스크립트 비동기 처리에 사용되는 객체이다.
Promise
객체의 기본 구조는 다음과 같다.
// 1️⃣ Promise 생성자 함수는 인자로 콜백함수를 전달 받는다.
const promise = new Promise((resolve, reject) => {
// 2️⃣ 전달 받은 콜백함수는 내부에서 비동기 작업을 수행한다.
// 비동기 작업을 수행한다.
// 3️⃣ 비동기 처리에 성공하면 인자로 전달받은 resolve 함수를 호출한다.
if (/* 비동기 작업 수행 성공 */) {
resolve('result');
}
// 4️⃣ 비동기 처리에 실패하면 reject 함수를 호출한다.
else { /* 비동기 작업 수행 실패 */
reject('failure reason');
}
});
Fulfilled
: 비동기 처리가 완료되어 Promise가 결과 값을 반환해준 상태로, Promise의 첫 번째 콜백함수인 resolve
가 실행되어 then
을 통해 처리값을 받을 수 있다.function greet(){
return new Promise(function(resolve, reject){
let message = "안녕!";
// 비동기 처리 완료로 응답!
resolve(message);
});
}
// 📍 greet()에서 Promise 객체가 리턴이 되고 then이라는 후속 처리 메서드를 이용해
// 📍 resolve의 매개변수로 들어간 message를 콜백함수의 매개변수인 resolvedData로 받아오게 된다.
greet().then(function(resolvedData){
console.log(greet());
console.log(resolvedData); // ✅ 안녕!
});
비동기 처리를 성공한 greet의 리턴 Promise의 상태
rejected
: 비동기 처리에서 실패 또는 오류가 발생한 상태로, Promise의 두 번째 콜백함수인 reject
가 실행되어 catch
을 통해 처리값을 받을 수 있다.function greet(){
return new Promise(function(resolve, reject){
let message = "처리해봐!";
// 비동기 처리 실패 또는 오류로 응답!
reject(new Error("오류가 발생했어요!"));
});
}
greet().catch(function(errorMsg){
console.log(greet());
console.log(errorMsg); // ✅ 오류가 발생했어요!
});
비동기처리를 실패한 greet의 리턴 Promise의 상태
💡 Promise의 후속처리 메서드
resolve
,reject
함수는Promise
객체를 반환하기 때문에 Promise로 구현된 비동기 함수는 Promise 객체를 반환하게 된다.
때문에 우리가 비동기 처리 결과 또는 에러메세지를 전달 받아 사용하기 위해서는 Promise 객체 그 자체를 사용할 수가 없고then
이나catch
같은 후속처리 메서드를 통해 전달 받아야한다.
여러개의 프로미스를 연결하여 사용할 수 있게 만드는 특징이다.
function greet(){
return new Promise(function(resolve, reject){
setTimeout(function(){
resolve('Hi!');
},2000);
});
}
function appreciate(){
return new Promise(function(resolve, reject){
setTimeout(function(){
resolve('Thank you!');
},2000);
});
}
greet().then(function(resolvedData){
console.log(resolvedData); // ✅ 'Hi!'
appreciate().then(function(resolvedData){
console.log(resolvedData); // ✅ 'Thank you!'
});
});-
다음과 같이 Promise를 리턴하는 함수 안에 Promise를 리턴하는 함수를 중첩해서 호출하는 방법으로도 순서를 제어할 수 있다.
하지만 이 방법보다는 Promise Chaining 방법을 주로 사용한다.
greet()
.then(function(resolvedData){
console.log(resolvedData); // ✅ 'Hi!'
return appreciate();
}
.then(function(resolvedData){
console.log(resolvedData); // ✅ 'Thank you!'
});
});-
이전 방법보다 훨씬 구조가 잘 들어온다.
다음과 같이 체이닝에 catch
를 연결해서 에러가 발생할 경우도 처리할 수 있다.
greet()
.then(function(resolvedData){
console.log(resolvedData); // ✅ 'Hi!'
return appreciate();
}
.catch(function (rejectData){
console.log('reject', rejectData);
}
.then(function(resolvedData){
// // ✅ appreciate()에서 비동기 처리가 성공한 경우라면 resolvedData에는 undefined가 들어올 것!
console.log(resolvedData);
});
});-
이렇게 Promise를 이용해서도 비동기처리를 할 수 있지만
catch
로 에러 처리를 해줘야하기 때문에 에러 처리가 매우 번거롭다.async-await
이 등장했다.자바스크립트의 비동기 처리 패턴 중 가장 최근의 나온 문법이다.
Promise를 활용하여 다음과 같이 적던 코드를
function greet(){
return new Promise(function(resolve){
const msg = "Hi!";
resolve(msg);
});
}
async-await을 사용하면 다음과 같이 조금 더 가독성 있게 적을 수 있다.
function sayHi() {
return "Hi"!
}
async function greet() {
const msg = await sayHi();
console.log(msg);
}
greet();
에러 처리도 다음과 같이 조금 더 수월해진다.
async function greet() {
try {
const msg = await sayHi();
console.log(msg);
}
catch(err) {
console.error(error);
}
}
REST를 기반으로 이루어진 API로 주소(url)을 통해 리소스를 식별하고, 리소스에 대한 행위는 HTTP Method(ex.get,post,patch 등)로 나타낸다.
REpresentatinal State Transfer의 약자로
이름에서 알 수 있다시피 자원을 이름(url)으로 구분하여 구상적(추상적의 반대 의미)으로 자원의 정보를 주고 받는 모든 것을 의미한다.
그리고 이 자원은 HTTP Method를 통해 처리된다.
REST의 특징으로는
1. Server-client 구조
: 자원을 가진 서버와 자원을 요청하는 클라이언트 구조로 되어있다.
2. Stateless
: HTTP 프로토콜이 Stateless Protocol, 즉 어떠한 이전 요청과도 무관하게 각각의 요청을 독립적인 트랜잭션으로 취급하는 프로토콜이기 때문에 REST 역시 그러하다. Client의 context 정보는 서버에 존재하지 않고, 클라이언트의 요청을 단순히 처리할 뿐이다.
3. Cacheable(캐시 처리 가능)
: 웹 표준 HTTP 프로토콜을 그대로 사용하기 때문에 웹에서 이용하던 기존의 인프라를 그대로 활용한다. 따라서 캐시 처리도 가능하며 이를 통해 응답시간이 빨라지고 전체 자원 활용이 효율적이다.
REST API를 제공하고, REST 원리를 따르는 웹 서비스를 RESTful
하다고 말한다.
RESTful은 성능을 높이는 것이 주목적이라기보다는 일관적인 컨벤션으로 API의 이해도와 호환성을 높이는 것이 주목적이다.
따라서 성능이 중요한 상황에서는 굳이 완전히 API가 RESTful할 필요는 없다.
예를 들어 CRUD 기능을 모두 post로만 처리하는 API는 컨벤션을 지키지 못했기에 RESTful하다고 할 수 없다.
GET
: 서버의 데이터를 읽는다.POST
: 서버에 데이터를 추가하고 등록한다.PUT
: 서버에 데이터를 쓴다.HEAD
DELETE
: 서버의 데이터를 삭제한다.PATCH
: 서버의 데이터를 수정한다. OPTION
: 서버가 해당 데이터에 대해 지원하는 HTTP METHOD를 취득한다.Trace
, Connect
와 같은 메소드가 존재한다. 서버에 요청을 보낼 때 함께 보내는 부가적인 정보를 의미한다.
쿼리스트링
: instance.get(
translate?input=${key}`)와 같이 ? 뒤에 붙여 서버에게 보내준다. param
: axios.get(
https://api.example.com/users/${userId}`)`와 같이 쿼리문 이전에 들어가는 파라미터를 말한다. 리액트에서는 useLocation()
, useParams()
로 이들의 정보를 받아올 수 있다.
application/json
const client = axios.create({
baseURL: ...
headers: {
Authorization: `Bearer ${getAccessToken('accessToken')}`,
'Content-type': 'application/json',
},
});
const requestBody = {
username: 'exampleUser',
password: 'examplePassword'
};
axios.post(`${apiUrl}/endpoint`, requestBody)
.then(response => {
console.log(response.data);
});
💡 Block / Non-block vs Sync - Async
1.Block
: 호출된 대상이 자신의 작업을 모두 마칠 때까지 제어권을 모두 갖고 있어 호출한 대상은 계속 대기한다.
2.Non-Block
: 호출된 대상이 자신의 작업을 마치지 않아도 제어권을 바로 반환해서 호출한 대상이 다른 일을 진행할 수 있다.
이렇게만 보면동기 & blocking
이 비슷하고,비동기 & non-blocking
이 비슷해보이지만동기/비동기
,blocking/non-blocking
두 그룹의 관심사가 다르다.
기다림(제어권 O)Blocking
/ 기다리지 않음(제어권 X)Non-blocking
내가 함 (내가 해서 기다림으로부터 자유로운 것)Synchronous
/ 다른 사람(콜백함수) 시킴Asynchronous
복합기의 작업을 예로 들어보자
1. sync/blocking
: 제어권이 계속 나에게 있어(blocking) 내가 그 제어권을 들고(sync) 일이 끝날 때까지 복합기 앞에서 기다려야한다.
2. sync/non-blocking
: 제어권이 계속 나에게 있지 않고 제어권을 가졌다가/반환했다가 작업을 반복하며 (이건 non-blocking) 제어권을 쥔 그 순간에 내가 직접 일을 수행해야한다(sync). 즉, 복합기의 작업이 완수가 됐는지 아닌지 주기적으로 확인해야하며, 그 사이사이에는 다른 작업을 할 수 있다. 하지만 그 다른 작업이 복합기의 작업이 완수되기 전에 종료되지는 못한다.
3. async/blocking
: 내가 아니라 심부름꾼에게 일을 맡긴다. (async) 복합기 작업이 종료될 때까지 심부름꾼은 복합기 앞에서 계속 기다리고 (blocking) 나는 다른 작업을 한다. 심부름꾼이 작업이 완료되면 와서 나에게 와서 알려준다.
4. async/non-blocking
: 역시나 내가 아니라 심부름꾼에게 작업을 맡긴다. (async) 심부름꾼은 이제 복합기 앞에서 계속 기다리지 않고 중간중간 다른 작업을 할 수도 있다. 다른 작업은 복합기의 작업보다 일찍 끝날 수도 있다. 그리고 나서 복합기의 작업이 완료가 되면 나에게 와서 알려준다.
참고자료
Promise | PoiemaWeb
JavaScript의 동작원리와 비동기처리
자바스크립트 Promise 쉽게 이해하기
promise와 async await의 차이점
안녕하세요 글 잘 읽고 갑니다!
혹시 DOM API부분의 setState는 어떤건 말씀하시는지 궁금합니다
react의 setState인가요?