JooBle
thumbnail
무한스크롤 구현하기
TIL
2022.02.10.

코루틴이란?

루틴

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번만큼만 받아와 로딩한뒤 화면을 렌더링하고, 다시 데이터를 받아와서 렌더링할 수 있기 때문에 빠르게 화면을 렌더링할 수 있다는 큰 장점을 가질 수 있다.


무한 스크롤을 예시로 코루틴을 이용해 구현해보도록 하자.

무한 스크롤

사진을 제공하는 서버를 이용해 간단하게 구현해보도록 하자.

큰 구조는

  1. 처음 사진 10개 로딩

  2. 스크롤이 바닥에 닿으면 다시 20개 로딩

  3. 무한 반복

정도로 볼 수 있다.

사용할 코드를 먼저 생각해보면

  1. 서버로부터 이미지를 받아올 dataLoding 함수.
  2. 이미지를 받으면 특정 횟수만큼 돌면서 그것을 렌더러에 전달할 함수.
  3. 받아온 데이터로 그림을 그릴 렌더러 함수.
  4. 무한스크롤을 원할 때 실행할 수 있는 함수

일단 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개씩 받아와 렌더링한다. */
  }
});

동작 확인

잘 동작하는것을 확인할 수 있다.

Have A Nice Day ! 🥳
© 2021 Developer Joo, Powered By Gatsby.