자바스크립트는 싱글스레드로 실행된다.
어떤 작업이 동기화가 된다면 작업이 완료될 때까지 기다려야 하기 때문에 사용자가 불편을 감수해야 한다.
따라서 자바스크립트는 동기화에 대한 다른 접근 방법이 필요하다
각각의 개념을 먼저 보고 이벤트 처리를 알아보자
콜백함수는 다른코드에 인수로 넘겨주는 실행 가능한 코드를 말한다.
콜백함수를 파라미터로 필요로 하는 메소드를 콜백메소드라고 한다.
아래 코드에서 getCurrentPosition()
는 콜백을 받는 콜백메소드
( gps 좌표를 받고나서( 콜백 ) alert 로 출력하게 된다 )
<script>
navigator.geolocation.getCurrentPosition(function (p) { // 콜백함수로 익명함수 사용
alert(p.coords.latitude);
alert(p.coords.longitude);
});
</script>
프로그램에서 발생하는 모든 이벤트는 이벤트리스너가 듣고 반응을 한다.
발생한 이벤트가 어떤 특정한 이벤트라면 동작할 함수를 등록해서 실행시킬 수 있다.
특정 이벤트의 종류는 클릭, 더블클릭, 드래그, 스크롤, 엔터 등 사용자가 할 수 있는 다양한 이벤트를 말한다
태그 안에 onclick="함수"
, 이나 dblclick="함수"
같은 리스너를 등록해서 이벤트 발생시 지정한 함수를 실행시킨다.
이때 이벤트리스너에 등록된 함수를 이벤트 핸들러라고 한다.
이벤트핸들러가 비동기 통신을 하면 사용자가 대기 시간을 느끼지 못하게 할 수가 있다
<!-- json 오브젝트를 받아보자 -->
<body>
<div id="postBox"> </div>
<button onclick="down()">다운로드</button>
<script>
function down(){
// 통신을 하려고 할때 자바스크립트가 제공해주는 fetch() 이용 - Fetch API
// fetch 리턴은 Promise 타입 -> response ( 프로미스 )
let response = fetch("http://localhost:8080/init/post",
{ // 두번째 파라미터는 선택적매개변수
method: "get" // 디폴트가 get , get 요청은 body가 없다
} );
console.log(response);
}
</script>
</body>
</html>
Fetch API
이용해서 데이터를 다운 받았다
HTTP 파이프라인을 구성하는 요청과 응답 등의 요소를 자바스크립트에서 접근하고 조작할 수 있는 인터페이스를 제공한다
( -> 내부적으로 소켓을 구현하여 I/O 통신을 가능하게 해준다 )
fetch
함수는 Promise
를 리턴한다
이미지를 다운 받는 예시 코드
fetch("이미지 URL")
.then((response) => response.blob())
.then((myBlob) => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement("img");
image.src = objectURL;
document.body.appendChild(image);
});
비동기 처리를 위한 하나의 패턴, 콜백지옥을 예방하기 위해 사용된다.
프로미스는 비동기처리의 상태정보를 갖는다
상태 | 의미 |
---|---|
pending | 비동기 처리가 아직 수행되지 않은 상태 |
fulfilled | 비동기 처리가 수행된 상태 (성공) |
rejected | 비동기 처리가 수행된 상태 (실패) |
settled | 비동기 처리가 수행된 상태 (성공 또는 실패) |
현 상태의 프로미스는 <pending>
상태로 되어 있다.
function down()
이 비동기 통신을 하지 않았기 때문이다.
기본적인 Promise 코드
첫번째 표현식의 반환 결과가 다음 줄의 표현식의 매개변수가 된다.
.then
은 이벤트 리스터와 유사하게 동작
chooseToppings()
.then((toppings) => placeOrder(toppings))
.then((order) => collectOrder(order))
.then((pizza) => eatPizza(pizza))
.catch(failureCallback);
모든 프로미스가 fulfilled
상태일때 실행 시킬 기능을 만들고 싶다면 Promise.all
이용
Promise.all([a, b, c]).then(values => {
...
});
프로미스를 리턴하는 fetch함수가 비동기 처리를 하기 위한 간단한 방법으로 async & await
을 이용하는 방법을 알아보자
요청 주소에 옵션으로 토큰을 보낸다면
async function down() {
let jwtToken = localStorage.getItem("jwtToken");
let response = await fetch("요청 URI", {
method: "get", // get 요청과 토큰을 보냄
headers: {
"Authorization": jwtToken
}
});
let jsObject = await response.json();
}
<body>
<div id="postBox"> </div>
<button onclick="down()">다운로드</button>
<script>
async function down(){
let response = await fetch("http://localhost:8080/init/post", {
method: "get"
} );
console.log(response);
}
</script>
</body>
</html>
비동기 처리가 필요한 함수 앞에 async
를 붙인다.
스레드는 현재 스택( down()
)에서 동기화상태로 머물지 않고 빠져나가 다른 일을 한다.
await
는 async
안에서만 동작하고 fetch
앞에 붙이면 함수의 context를 남긴다. 쉽게 말해 체크포인트를 남긴다
await
는 프로미스가 처리될 때까지 리스너가 함수 실행을 기다리게 만들고 fetch
가 완료 되어 프로미스가 처리되면 콜백을 해준다
이후 스레드가 할 일을 끝낸 뒤 여유가 있을때 체크포인트( context ) 를 확인한다.
즉, 리스너가 콜백을 받고 다시 내부 스택으로 돌아와서 작업을 진행한다
결과
본래 원하는 결과는 JSON 오브젝트이기 때문에 추가 작업이 필요하다
<!-- HTML 에 테이블 추가후 -->
<script>
async function down() {
let response = await fetch("http://localhost:8080/init/post", {
method: "get"
});
let jsObject = await response.json(); // 응답받은 데이터를 json 으로 바꿔서 저장
let makeTr = document.createElement('tr');
makeTr.innerHTML = `
<td>${jsObject.data[0].id}</td>
<td>${jsObject.data[0].title}</td>
<td>${jsObject.data[0].content}</td>
<td>${jsObject.data[0].user.username}</td>
`;
let target = document.querySelector('#postBox-table');
target.append(makeTr);
}
</script>
.json()
을 이용하면 프로미스를 쉽게 json 으로 파싱
createElement()
는 html에 태그를 생성
makeTr.innerHTML
에 문자열 리터럴을 사용해 레코드 추가
append()
로 html 에 추가 ( DOM 조작 )
버튼을 누르면 데이터 다운받아짐
지금 코드는 책임이 집중되어 있으므로 분리시키자
<script>
async function down() {
let response = await fetch("http://localhost:8080/init/post", {
method: "get"
});
let jsObject = await response.json();
render(jsObject);
}
function render(jsObject) {
let tableEl = document.querySelector("#postBox-table");
for (let i = 0; i < jsObject.data.length; i++) {
let tr = makeTr(jsObject.data[i]);
tableEl.append(tr);
}
}
function makeTr(post) {
let tr = document.createElement("tr");
tr.innerHTML = `
<td>${post.id}</td>
<td>${post.title}</td>
<td>${post.content}</td>
<td>${post.user.username}</td>
`;
return tr;
}
</script>
추가적으로 정리
json.parse()
- json 을 js 오브젝트로 변환
JSON.stringify()
- js 오브젝트를 json 으로 변환 ( JSON 대문자 !! )
fetch()
는 프로미스를 리턴
프로미스.json()
-> json 으로 변환
I/O처리를 해야 할때 동기화를 피할 방법으로 브라우저( 자바스크립트 엔진 )는 비동기 통신을 이용한다
위와 같은
async - await
/jQuery
/Ajax
등을 이용
브라우저는 사용자의 이벤트를 관찰하고 있는 리스너를 가지고 있다.
리스너는 while 이 되면 동기화가 되므로 아주 짧은 시간 동안만 멈춘다.
이벤트가 발생하면 브라우저는 이벤트를 이벤트큐에 등록하게 된다.
브라우저가 해야 할 작업( 렌더링 같은 )이 모두 종료 되고 나서 할 일이 없을때 등록된 이벤트를 순차적으로 실행한다.
개발자는 이벤트를 처리할 타겟( 콜백함수 = 이벤트 핸들러 )을 등록하기만 하면 리스너가 알아서 동작하게 된다.
리스너가 관찰하고 작업을 처리하는 콜스택이 비었을때 이벤트큐의 작업을 가져와 처리하는 과정을 이벤트 루프
라고 한다.
자바스크립트 엔진은 단순히 작업이 요청되면 Call Stack을 사용하여 요청된 작업을 순차적으로 실행할 뿐이다. 비동기 요청 처리는 브라우저( 또는 Node.js )가 담당한다.
CSR / SSR
자바스크립트의 비동기 통신을 이용해서 브라우저가 그때 그때 렌더링 하는것을 CSR
이라고 한다.
서버에서 렌더링을 하는 SSR
은 요청이 많을수록 서버에 부하가 많이 가기 때문에 최근에는 페이스북같은 많은 요청이 필요한 웹에서 CSR 을 많이 사용한다
자바같은 경우 멀티스레드를 사용하여 보다 빠르게 동작할 것처럼 예상되지만 실제로는 수많은 컨텍스트 스위칭으로 인해서 오히려 단일스레드보다 늦어질 수가 있는데 자바스크립트를 이용한 서버( Node.js )가 최근 많이 사용되는 이유이기도 하다 ( 웹에 최적화된 NIO )
자바에서 콜백함수를 만들어 보자
자바는 통신을 할때 멀티 스레드를 이용한다.
작업이 끝날때까지 기다리는 join()
은 잘 사용하지 않고 콜백함수를 만들어 이용한다.
public interface Callback {
void check(String data);
}
public class Downloader {
Callback callback; // Composition
String data = "";
public Downloader(Callback callback) {
this.callback = callback;
}
public void down() {
System.out.println("다운시작");
new Thread(() -> {
try {
Thread.sleep(5000);
System.out.println("다운종료");
data = "바나나";
callback.check(data); // 콜백함수
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
public class App {
public static void main(String[] args) {
Downloader d1 = new Downloader((data) -> { // 생성자, 람다식으로 리스너 작성
System.out.println(data);
});
d1.down();
System.out.println("대기중...");
}
}
// < 결과 >
// 다운시작
//대기중...
// < 5초후 >
// 다운종료
// 바나나
이러한 과정을 리스너를 등록한다고 말한다