이 글은 React Fiber에 대하여 다루지 않습니다.
솔직히 말씀드리면 React Fiber는 저도 잘 모릅니다.
다만 이 글을 다 읽으시면 React Fiber가 무엇인지 어렴풋이 아실 수 있을 겁니다.
React Fiber에 대한 글은 많지만 Fiber를 제대로 설명하는 글은 드뭅니다.
아무래도 독자가 Fiber를 이미 알고 있다고 가정하고 작성했거나, Fiber를 적기에는 너무 글이 길어질까봐 그랬을 수도 있을 것 같습니다.
하지만 Fiber를 모르면 React Fiber에 대해 제대로 설명할 수 없습니다. 마치 Javascript를 모르는 사람이 React에 대해 설명하는 격이죠.
이 글에서는 Fiber에 대해 다룹니다. 아니, 사실 Fiber에 대해서도 잘 다루지 않고 어떠한 예제를 보여드리고 이것이 어떻게 활용될수 있는지만 알려드릴겁니다.
fiber의 정의는 user level thread라고 할 수 있습니다.
주의할점은 thread 라는 단어가 들어갔다고 해서 일반적인 스레드가 아닙니다. (코드도 동시에 실행되지 않습니다)
Fiber의 정의는 여기 에 있고 제 방식대로 다시 설명하자면...
user level
이란 뜻은 운영체제 혹은 하드웨어가 해주는일을 내가 코드로 대신 하겠다는 의미도 있습니다. 지금까지는 운영체제가 작업을 스케쥴링 해 줬다면 이제는 내가 직접 뭐가 실행될지 제어하겠다 라고 보시면 됩니다. 근데 이거보다는 작업을 여러 단위로 분리한다
이 관점이 더 중요합니다.
대부분 접하고 계시는 자바스크립트의 비동기 함수들은 io-bound task입니다.
그냥 네트워크나 파일 요청이 끝날때까지 대기하는 작업들이죠.
io-bound / cpu-bound에 대한 설명은 여기에 있습니다.
그러면 반대로 CPU를 많이 잡아먹는 오래 걸리는 연산을 UI 블럭없이 처리하려면 어떻게 해야할까요?
여기서는 CPU를 많이 잡아먹는 작업을 fibo(피보나치)
함수로 예시로 들어보겠습니다.
const fibo = (n: number) => {
if (n === 0) return 0;
if (n === 1) return 1;
return fibo(n - 1) + fibo(n - 2);
}
이 함수는 간단하지만 꽤나 비쌉니다! 만약 우리가 n에 적당히 큰 숫자를 넣는다면 작업은 정말 오래 걸릴것이고, 결과값이 나오는 동안 chrome 탭은 응답 없음 상태가 될겁니다. 심지어는 아예 안끝나고 탭이 죽어버릴수도 있습니다.
const fibo = async (n: number) => {
/* ??? */
}
그러면 우리는 이 문제를 해결하기 위해 fibo
함수를 async로 만들어야 할 필요가 있습니다. 이게 대체 어떻게 가능할까요?
아래 코드는 위에서 말한것처럼 fibo함수 내부의 작업을 쪼개서 스케쥴러에 던진 후, 스케쥴러가 매 이벤트 루프마다 작업을 실행하도록 합니다.
let jobQueue = [];
const pushJob = (job) => {
let resolve;
const p = new Promise((_resolve) => {
resolve = _resolve;
});
const jobItem = {
exec: async () => {
resolve(await job());
},
promise: p,
};
jobQueue.push(jobItem);
return p;
};
const fibfibo = async (n) => {
if (n === 0) return 0;
if (n === 1) return 1;
const f1 = await pushJob(() => fibfibo(n - 1));
const f2 = await pushJob(() => fibfibo(n - 2));
return f1 + f2;
};
// 이건 가장 간단한 수준의 스케쥴러입니다!
setInterval(() => {
const copy = jobQueue;
jobQueue = [];
copy.forEach((job) => {
job.exec();
});
}, 1);
원래 fibo(n)
함수에서 가장 느린 부분은 fibo(n-1)
그리고 fibo(n-2)
였습니다.
이 부분을 하나의 태스크로 만들어서 즉시 실행하는 대신 스케쥴러에게 미뤘습니다.
즉시 실행하는 버전의 fibo(n)
의 소요시간은 fibo(n-1) + fibo(n-2)
였지만, 이제는 작업이 쪼개졌기 때문에 간단한 if문 처리와 덧셈 시간 정도로 줄어들었습니다!
그리고 이 작업을 오랜 시간에 걸쳐 반복하면(스케쥴러가) 우리가 원하는 결과를 도출할 수 있습니다
이 코드의
fibfibo
함수는 정말 끔직하게 느립니다 하지만 적어도 UI를 멈추게 하지는 않습니다.이번 예제에서는 속도가 중요한건 아니니 일단 이 코드를 가지고 계속 진행해보도록 하겠습니다.
우리는 위에서 가장 간단한 수준의 스케쥴러를 구현했습니다.
그냥 작업이 들어오면 순서대로 다 실행해보리는 코드라 사실 스케쥴러라고 하기도 애매하지만, 이 부분을 수정한다면 다른 재밌는 일을 해볼수도 있습니다.
이미 실행중인 작업 취소하기
const cancelTask = (taskID: number) => {
jobQueue = jobQueue.filter(x => x.taskID !== taskID);
};
cosnt fibProm1 = fibfibo(100);
const fibProm2 = fibfibo(200);
// 더 이상 필요 없어진 작업을 중간에 취소할 수 있습니다!
cancelTask(fibProm1.taskID);
작업에 우선순위 부여하기
// jobQueue를 배열이 아닌 우선순위 큐로 교체합니다.
let jobQueue = new PriorityQueue();
// 우선순위를 조절할 수 있습니다!
fibfibo(200, { priority: 'high' });
fibfibo(100, { priority: 'normal' });
React는 최근에 Concurrent Mode(사실 이 이름은 폐기되었습니다)를 도입해 작업이 많은 경우에도 빠른 UI 반응성을 확보할 수 있도록 했습니다. 이것은 우리가 알아본 내용과 어떠한 관련이 있을까요?
아니면 이미 위 응용 예제를 보고 눈치채신 것이 있으신가요?
React 문서 에는 지금까지 우리가 다룬 내용과 거의 일치하는 문장이 있습니다. (concurrent mode라는 용어가 드랍되면서 한국어 문서도 같이 없어진것 같아서 대충 번역기만 돌렸습니다)
React가 이러한 작업 중단
혹은 우선순위 렌더링
을 수행하는것은 흑마법이 아닙니다! 왜냐하면 우리도 이미 중단 가능한 fibo
함수를 만들어냈기 때문이죠.
중단 가능한 작업
(fiber) 단위로 쪼개버립니다.yield
혹은 스케쥴링에 관여하지 않고도 중단 가능한 렌더링을 구현할 수 있도록 되어있습니다. (좋네요)비동기 fibo
함수를 구현하고 태스크 취소 기능도 구현해보았습니다.
숫자를 입력하고 GO
버튼을 누르면 UI가 블럭되지 않고도 값을 계산해서 출력합니다.
만약 계산중에 다른 실행이 일어나면(계산중일때 다시 계산시키면) 이전 계산은 취소합니다.
https://jake.stackblitz.com/edit/react-xsggbf?file=src%2FApp.js
위에도 언급했지만 이 간단한 구현체는 정말 끔찍하게 느립니다.
제 컴퓨터에서는 15 정도의 값을 넣어야 적당한 시연이 가능했으니 참고해주세요.
위에서 우리가 작성한 코드는 파이버일까요?
아쉽게도 파이버는 아닙니다. 제가 작성한 코드는 React Fiber 그리고, Fiber 그 자체를 이해하기 위한 아주 간단한 버전의 작업 쪼개기와 스케쥴러일 뿐입니다. 물론 이 자체로도 파이버라고 할 수도 있겠지만 진짜 파이버는 아주 조금 더 다르게 생겼습니다.
아래는 참고해볼만한 fiber의 예시들입니다.
여기까지 읽으셨으면 이제 React Fiber에 대해서 읽어볼 준비가 되었습니다!
https://www.alibabacloud.com/blog/a-closer-look-at-react-fiber_598138
이 글은 '작업 분할과 스케쥴링' 관점에서의 Fiber에 대해 다룹니다.
(처음만 읽다가 쭉 내려서 마지막의workloop
코드만 보셔도 됩니다)