본문 바로가기

호기심 천국

리액트 동시성이란

참새 뒷모습. Unsplash 에 Niklas Ohlrogge 님이 올림.

들어가는 말

이 글은 리액트의 동시성이란 무엇인지 기본 개념을 가볍게 공부해서 정리한 내용입니다. useTransition, <Suspense> 처럼 실제 동시성 모드를 사용하는 기능을 어떻게 쓰는 것인지 또는 이 구체적인 원리가 무엇인지는 다루지 않습니다.

요약

왜 생겼는지

어떤 컴포넌트의 렌러링을 위한 연산 과정이 길 때, 다른 컴포넌트의 렌더링 역시 늦춰지는 문제를 해결하기 위해서에요.

이게 뭔지

방해 가능(interruptible)한 렌더링을 통해 우선순위가 높은 렌더링을 먼저 실행할 수 있게끔 합니다.

뭘 할 수 있는지

현재 보여지고 있는 화면의 상호작용성을 막지 않으면서, 뒤에서 몰래 다음 화면의 렌더링 작업을 수행할 수 있습니다.

어떻게 만들었는지

여러 작업들이 있을 때 우선순위에 따라 기존 작업을 중단하고 새로운 작업을 실행하는 등의 기능을 위해 스케줄러를 만들었어요. 렌더링 단계를 fiber 라는 작은 단위로 나누고, 우선순위를 부여하여 스케줄러에서 원활하게 사용할 수 있습니다.

소개

리액트 18버전에서 선보인 동시성은 사실 우리가 생각하는 '신 기능'은 아닙니다. 우리가 동시성을 대놓고 쓴다기보다는 리액트 쪽에서 렌더링 계산을 할 때 쓰는 새로운 매커니즘에 가깝거든요. 따라서 일반적인 개발 상황에서 동시성 자체를 고려할 일은 많이 없으리라는 것이 리액트 측의 생각입니다.

참고: 동시성과 병렬성

동시성은 여러 개의 작업들을 하나의 스레드에서 어떻게 번갈아가면서 수행합니다. 그래서 여러 작업을 동시에 처리하는 것처럼 보이게끔 하는 것이 목표에요. 병렬성은 하나의 작업을 여러 스레드에 나눠서 수행합니다. 그래서 어떻게 나눠서 연산을 수행하고, 그 결과들을 어떻게 합칠 것인지를 고민해야 해요.

등장

옛날 리액트 렌더링은 시작하면 절대 멈출 수 없었어요. 맛집 대기줄마냥 정직했죠. 그래서 앞집 렌더링이 길어지면 뒤쪽 컴포넌트들은 무조건 기다려야 했어요. 싱글 스레드니까 당연히 렌더링 도중에는 어플리케이션 사용자 역시 아무 것도 할 수 없었죠. 리액트의 동시성 모드는 이런 render-blocking 현상을 해결하고자 했습니다. 실제로 동시성 모드를 사용하는 여러 기능들을 살펴보시면 궁극적으로는 이 문제를 해결하고자 했다는 걸 볼 수 있어요.

핵심

리액트 동시성 모드의 핵심은 렌더링이 방해 가능(interruptible)하다는 것입니다. 그렇기 때문에 렌더링을 시작하고 도중에 멈췄다가 나중에 다시 그 지점부터 실행하는 게 가능해요. 심지어는 진행 중인 작업들을 몽땅 없던 일처럼 폐기할 수도 있습니다. 렌더링이 멈췄더라도 UI에서는 별 문제가 없는 것처럼 보이게 해 줘요.

사용

그렇다면 동시성으로 뭘 할 수 있을까요? 리액트 18 소개글에서는 크게 두 가지 용례를 보여줍니다.

먼저 UX 개선입니다. 렌더링 이후 실제 DOM을 변형시키는 과정은 모든 컴포넌트 트리의 계산이 끝날 때까지 미룹니다. 그리고 사용자가 화면을 보고 있는 동안 뒤에서 몰래 새로운 화면들을 준비하죠. 혹시 사용자가 화면과 상호작용을 해서 즉시 어떤 변화가 생겨야 한다면 몰래 준비하던 작업을 멈추고, 즉각적인 변화를 먼저 렌더링해서 보여준 후 다시 뒷작업을 시작합니다. 사용자 입장에서는 이상한 점을 전혀 느끼지 못하는 것이죠. 만약 동시성이 없었다면 뒷작업이 아닌 '앞작업' 이었을 테고, 사용자가 어떤 행동을 해도 먼저 하고 있던 앞작업이 끝나기 전까지는 다른 걸 할 수 없으니 사용자 입장에서는 화면이 멈춘 것처럼 느껴졌겠죠.

다음으로는 재사용 가능한 상태(reusable state) 입니다. 이 부분은 구체적인 설명이 없어서 저도 잘 모르겠습니다. 추측컨대 뒤쪽에 뭔갈 숨겨놓고 작업할 수 있다는 점을 응용해서 기존 화면이 필요 없을 때 뒤에 숨겼다가 다시 필요해지면 그걸 바로 꺼내서 쓸 수 있다는 의미 같아요. 리액트가 두 개의 가상 돔을 사용한다는 점을 보면 하나의 가상 돔이 실제로 사용되면 나머지 하나를 백지화하는 게 아니라 그대로 갖고 있다가 다시 그걸 그대로 보여줘도 된다는 의미가 아닐까요?

적용

우리가 어떻게 하면 동시성 렌더링을 적용할지는 몰라도 됩니다. 동시성 모드는 동시 렌더링을 지원하는 새로운 기능을 사용할 때 알아서 켜지거든요. 궁극적으로 리액트에서 원하는 것은 프레임워크나 라이브러리에서 동시성을 쓰고, 그걸 다시 개발자가 쓰는 방향을 원합니다. 유틸 함수를 좋아하시는 분들이라면 아시겠지만 '사용하기 편한 완벽한 블랙박스'를 목표로 하는 것 같아요. 개발자는 '무엇을' 만들 것인지에 집중하고 리액트는 '어떻게' 그걸 만들어 줄 것인지 생각하는 거죠. 동시성 렌더링은 후자에 더 가깝습니다.

구현

동시성을 구현하기 위해서 리액트는 크게 두 가지를 바꿨습니다.

스케줄러

기존 렌더링에는 일시정지라는 말이 아예 없었습니다. 하지만 리액트 동시성의 핵심은 렌더링의 일시정지입니다. 그래서 리액트 쪽에선서는 렌더링을 위한 스케줄러를 만들었어요. 일정 주기로 메인 스레드에 작업을 양보하고, 또 본인이 해야 하는 작업들 중 우선 순위에 맞춰서 실행할 수 있도록 말이죠.

fiber

그럼 스케줄러가 하는 작업이 도대체 뭔지는 누가 정할까요? 그건 fiber가 알고 있습니다. 우리가 밥을 먹다가 잠깐 다른 일을 해야 한다고 해 봅시다. '식사의 중단'은 어느 시점에 일어나야 할까요? 숟가락을 들고 있거나 입에 물고 있을 때가 아니라 일단 한 입 하고 내려놓은 뒤에 하는 게 일반적입니다. 즉 식사를 함에 있어서 숟가락을 들고, 먹고, 내려놓는 걸 하나의 과정이라 하고 이 과정들을 반복해서 밥을 다 먹겠죠. 만약 중단해야 한다면 하나의 과정이 끝나고 다음 과정이 시작하기 전이 될 겁니다.

리액트에서는 이 과정 단위 하나를 fiber 라고 칭합니다. 스케줄러를 잘 이용하기 위해서는 큰 작업의 일시정지 및 복귀, 작업별 우선순위 지정, 이전 작업물 재사용과 같은 일들이 원활하게 일어나야 해요. 그러기 위해서는 결국 큰 작업물을 작은 단위로 나눠야 하는데, 그 단위가 fiber 입니다. fiber 단위로 작업을 진행하고, 혹시 우선 순위가 높은 게 있다면 이 fiber가 끝난 뒤 더 중요한 fiber를 실행하면 됩니다. fiber 안에 우선 순위를 나타낼 수 있는 표식을 넣어 놓는다면 우선순위 역시 아무런 문제가 되지 않습니다. 두 개의 fiber를 활용해 누가 현재 보여지고 있는 진짜인지를 정한다면 다음 fiber 작업 수행 시 혹시 아무 일도 없어도 된다면 이전 작업을 그대로 기억해서 가져올 수도 있겠죠.

참고 자료

React docs: React v18.0
React Conf 2021: React 18 Keynote
Naver D2: Inside React(동시성을 구현하는 기술)
React legacy docs: Design Principles: scheduling
acdlite: React Fiber Architecture
Naver D2: React 파이버 아키텍처 분석