코루틴이란?
루틴
routine은 반복적으로 실행시킬 수 있는 명령을 말한다. routine은 한번 실행되면 명령이 끝날때까지 개입할 수 없는것이 특징이다.
코루틴
co-routine의 co는 반복을 의미한다. co-routine의 특징은 routine과 달리 명령이 끝나기 전이라도 진입할 수 있고 반환할 수 있다는 것이다. 즉, suspend + resume의 기능을 갖추고 있다.
suspend는 동기명령을 일시적으로 멈추는 것을 의미하고 resume은 멈춘 부분을 메모리에 저장한 후 그 다음부터 다시 실행하는 것을 의미한다. 자바스크립트에서 코루틴은 generator를 통해 지원된다.
예를들어 합계를 계산하는 아래의 코드가 있다고 해보자.
const f1 = () => {
let num = 100000;
let acc = 0;
while (num > 0) {
acc = acc + num;
num = num - 1;
}
return acc
}
const res1 = f1()
console.log(res1)
for문이 10만번도는 동안 중간에 개입할 수 없기 때문에 오랜시간동안 사용자는 명령이 끝날때까지 기다리는 수 밖에 없다. 이것을 제너레이터를 이용해 바꿔보면
const f2 = function* () {
let num = 100000;
let acc = 0;
while (num > 0) {
acc = acc + num;
num = num - 1;
if (num % 100 == 0) yield acc
}
return acc
}
const res2 = [...f2()]
console.log(res2)
이런식으로 바꿀 수 있다.
제너레이터로 구현한 f2의 경우엔 10만번을 100번씩 쪼개서 실행한것과 같다. 즉 1000번 나갔다 들어왔다를 반복하며 res2에 계속해서 acc를 반환해준다.
코루틴은 서버에서 데이터를 받아 처리할때 유용하게 사용될 수 있다. 서버로부터 몇천 몇백개의 데이터를 한번에 받는다고 생각하면, 모든 데이터를 로딩할동안 브라우저는 blocking 상태에 빠질것이고 사용자의 이탈율은 높아지게된다.
코루틴을 이용하면 몇 백개의 데이터를 n번만큼만 받아와 로딩한뒤 화면을 렌더링하고, 다시 데이터를 받아와서 렌더링할 수 있기 때문에 빠르게 화면을 렌더링할 수 있다는 큰 장점을 가질 수 있다.
무한 스크롤을 예시로 코루틴을 이용해 구현해보도록 하자.
무한 스크롤
사진을 제공하는 서버를 이용해 간단하게 구현해보도록 하자.
큰 구조는
-
처음 사진 10개 로딩
-
스크롤이 바닥에 닿으면 다시 20개 로딩
-
무한 반복
정도로 볼 수 있다.
사용할 코드를 먼저 생각해보면
- 서버로부터 이미지를 받아올 dataLoding 함수.
- 이미지를 받으면 특정 횟수만큼 돌면서 그것을 렌더러에 전달할 함수.
- 받아온 데이터로 그림을 그릴 렌더러 함수.
- 무한스크롤을 원할 때 실행할 수 있는 함수
일단 4가지 정도로 생각할 수 있다.
const el = (v) => document.createElement(v) // 유틸함수
const loadImg = async (page) => { // 1번
return await fetch(`url`+ page)
}
const infinityScroll = function*(){} // 2번
const render = (src) => { // 3번
const imageContainer = document.querySelector('.container');
const img = imageContainer.appendChild(el('img'));
img.src = src;
}
const exec = infinityScroll() // 4번
/* HTML */
<div class="container"></div>
/* CSS */
.container {
display: grid;
grid-template-columns: repeat(4,1fr);
}
img {
width: 200px;
height: 200px;
}
코드를 구현해보자.
const el = v => document.createElement(v);
const err = msg => {
throw new Error(msg);
};
const infinityScroll = async function* () {
try {
let page = 1;
while (true) {
const src = await loadImg(page);
/* loadImg는 Promise를 리턴하므로 await으로 받아주자. */
if (!src) {
err(`Invalid src: ${src}`);
break;
} else {
yield src;
page++;
}
}
} catch (err) {
console.error(err.message);
}
};
const loadImg = async page => {
// 바로 url을 넘겨준다.
return (await fetch(`https://picsum.photos/id/${page}/350/350`)).url;
};
/* 한번 실행된 제너레이터는 재사용할 수 없음에 유의하자 */
const gene = infinityScroll();
const exec = async (cnt = 10) => {
try {
if (cnt < 0 || isNaN(cnt)) err(`Invalid cnt : ${cnt}`);
/* 기본적으로 10개씩 로딩하고 인자를 지정할 수 있다.*/
for (let i = 0; i < parseInt(cnt); i++) {
/* gene역시 promise를 반환하므로 await 처리를 해주자.*/
const { done, value } = await gene.next();
if (!done) render(value);
}
} catch (err) {
console.error(err.message);
}
};
const render = src => {
const imgContainer = document.querySelector('.container');
const img = imgContainer.appendChild(el('img'));
img.src = src;
};
/* 처음에 이미지 10개를 받아와 렌더링하고 */
exec();
document.addEventListener('wheel', () => {
if (window.scrollY + window.innerHeight >= document.body.clientHeight) {
exec(5);
/* 이후엔 20개씩 받아와 렌더링한다. */
}
});
동작 확인
잘 동작하는것을 확인할 수 있다.