React Native Worklets 개념 정리

React Native Worklets 개념 정리

2026년 08월 23일

들어가기 전에

Reanimated나 Gesture Handler 코드를 보다 보면 'worklet'이라는 지시문이나 useAnimatedStyle 같은 훅 안에서 실행되는 함수를 마주칩니다.
이 시리즈에서는 Reanimated의 기반이 되는 Worklets 개념을 기초부터 실전 주의사항까지 다룹니다. 이번 글에서는 "Worklet이 뭐고 왜 필요한가", 그리고 그 바탕이 되는 Thread와 Runtime의 관계를 정리합니다.

이 글은 Reanimated 4 / React Native New Architecture 기준으로 설명합니다. Reanimated 4부터는 Legacy Architecture를 지원하지 않습니다.

React Native 렌더러는 스레드를 몇 개 쓸까

New Architecture의 렌더러는 두 개를 씁니다.

스레드역할
UI Thread (Main Thread)네이티브 뷰를 조작할 수 있는 유일한 스레드
JavaScript ThreadReact의 render phase와 layout이 실행되는 스레드

"The renderer uses two different threads: UI thread (often called main): The only thread that can manipulate host views. JavaScript thread: This is where React's render phase, as well as layout, are executed."
React Native Threading Model

여기서 "두 개"는 렌더러 기준입니다. 앱 전체로 보면 Native Modules 호출을 처리하는 스레드 등이 더 있습니다. 구 아키텍처에는 Yoga 레이아웃을 전담하는 Shadow Thread가 하나 더 있었습니다.

프레임 예산은 빠듯합니다. 60fps 기준 한 프레임이 약 16.67ms, 120fps 기준으로는 약 8.33ms입니다. 애니메이션이나 제스처처럼 매 프레임 반응해야 하는 작업은 이 안에 끝나야 합니다.
그런데 이 로직은 React render, 상태 관리, 네트워크 응답 처리와 JS Thread를 나눠 씁니다. JS Thread가 바쁜 순간에는 애니메이션이나 제스처 반응이 그만큼 밀립니다.

그래서 Worklet이 필요했다

Worklet의 아이디어는 단순합니다. 프레임에 민감한 계산을 붐비는 JS Thread에 맡기지 않고 UI 쪽에서 바로 처리합니다.

"Worklet is a short-running JavaScript function that can be moved and executed across different Worklet Runtimes and the React Native Runtime."
Glossary of Terms

정의는 "UI 스레드"가 아니라 "여러 Runtime 사이를 옮겨 다닌다"입니다. 가장 흔한 용도가 UI 쪽 처리인 건 맞습니다. 하지만 Worklet 자체는 그보다 일반적인 개념입니다. UI 전용 코드로 못 박아두면 잠시 뒤 나올 Worker Runtime 이야기에서 막힙니다.

이 정의에는 아직 설명하지 않은 단어가 하나 있습니다. Runtime입니다.

Thread와 Runtime은 다른 개념이다

Runtime은 JS 코드를 실행하는 독립된 실행 환경(변수, 함수, 글로벌 스코프를 가진 하나의 JS 세계)을 말하고 Thread는 OS 레벨의 실행 단위입니다. 이 둘은 1:1로 묶여있지 않습니다.

"It isn't coupled to any thread, but there might be a thread dedicated to its execution."
Runtime Kinds

Worklets 라이브러리는 런타임을 두 갈래로 나눕니다. RN RuntimeWorklet Runtime입니다. Worklet Runtime은 다시 UI RuntimeWorker Runtime으로 나뉩니다.

RN Runtime          ← React Native가 만드는 런타임
Worklet Runtime     ← Worklets 라이브러리가 만드는 런타임
├─ UI Runtime
└─ Worker Runtime

"A JavaScript Runtime spawned by Worklets library. It's pre-configured to be able to execute worklets. It doesn't share any memory with the React Native Runtime or other Worklet Runtimes, but it can communicate with them using specific APIs."
Runtime Kinds

세 런타임은 개수와 맡는 일이 서로 다릅니다.

런타임실행 스레드개수접근 가능한 것
RN RuntimeJS Thread앱당 1개React, 앱 상태, RN API에 접근 가능한 유일한 런타임
UI Runtime주로 UI(Main) Thread앱당 1개애니메이션 계산, 네이티브 이벤트 동기 처리 등 고우선순위 작업
Worker Runtime별도 스레드여러 개 가능createWorkletRuntime으로 직접 생성, 무거운 연산용

"There's only one React Native Runtime in your app." / "There's only one UI Runtime in your app." / "There can be multiple Worker Runtimes in your app running at the same time."
Runtime Kinds

UI Runtime은 다른 스레드가 잠깐씩 빌려 쓸 수 있습니다.

"UI Runtime can be periodically acquired by other threads (other runtimes) to synchronize data."

런타임은 각자 자기만의 글로벌 스코프와 힙이 있습니다. 앞선 Worklet Runtime 정의에 나온 "메모리를 공유하지 않는다"는 말의 실체가 이것입니다. 다음 편에서 다룰 클로저 동작 차이의 근본 원인이기도 합니다.

UI Runtime이 도는 UI Thread는 네이티브 렌더링도 함께 처리하는 스레드입니다. Worklet이 JS Thread의 혼잡을 피하게 해주는 건 맞지만 아무도 쓰지 않는 한가한 전용 스레드로 옮겨가는 건 아닙니다.
그래서 UI Thread 자체가 바쁘거나 네이티브 뷰가 너무 많아 레이아웃·드로잉 비용이 크면 Worklet을 써도 프레임 드랍은 발생할 수 있습니다. "Worklet을 쓰면 무조건 60fps"가 아니라 "JS Thread가 막혀도 애니메이션이 계속될 수 있다" 정도로 이해하는 게 정확합니다.

런타임을 여러 개 만들 수 있는 이유: JSI

JSI(JavaScript Interface) 덕분입니다.

구 아키텍처에서 JavaScript와 네이티브는 Bridge 하나로만 통신했습니다. 비동기이고 JSON으로 직렬화되고 배치로 묶이는 구조였습니다.
New Architecture는 이 Bridge를 걷어내고 JSI로 대체했습니다.

JSI를 "JS와 네이티브를 잇는 통신 채널" 정도로만 이해하기 쉽습니다. 하지만 공식 용어집의 정의는 조금 더 넓습니다.

"A lightweight API to embed a JavaScript engine in a C++ application."
Glossary

JS 엔진 자체를 C++ 안에 심는 API입니다. 앞에서 본 UI Runtime과 Worker Runtime이 만들어질 수 있는 근거가 여기 있습니다. Worklets는 이 API로 JS 엔진(React Native의 기본 엔진인 Hermes) 인스턴스를 하나 더 띄워서 런타임으로 씁니다.

다만 "JSI가 곧 worklet을 실행한다"는 표현은 정확하지 않습니다. JSI는 런타임을 만들고 다루는 토대일 뿐입니다. 일반 함수를 다른 런타임에서 실행 가능한 형태로 바꾸는 건 바로 다음에 볼 Babel 플러그인의 몫입니다.

Worklet은 어떻게 다른 Runtime으로 옮겨가는가: Workletize

일반 함수는 그냥 선언하면 되지만 Worklet은 직렬화 가능한 객체로 변환되는 과정을 거쳐야 다른 Runtime으로 복사되어 실행될 수 있습니다. 이 과정을 workletize라고 부릅니다.

"To convert a JavaScript function into a serializable object which can be copied and run over on Worklet Runtimes."
Glossary of Terms

'worklet' 지시문이 붙은 함수는 Worklets Babel 플러그인이 빌드 시점에 자동으로 변환해줍니다.

function calculateStyle(progress: number) {
  "worklet";
  return {
    opacity: progress,
    transform: [{ translateY: (1 - progress) * 12 }],
  };
}

다만 'worklet'을 항상 직접 붙여야 하는 건 아닙니다. useAnimatedStyle, gesture 콜백처럼 Reanimated/Gesture Handler가 제공하는 컨텍스트 안에서는 콜백이 자동으로 workletize되기 때문입니다.

const x = useSharedValue(0);
 
// 'worklet' 지시문 없이도 자동으로 UI Runtime에서 실행됨
const animatedStyle = useAnimatedStyle(() => {
  return { transform: [{ translateX: x.value }] };
});

이 자동 변환이 어디까지 닿는지가 중요합니다. 경계를 모르면 "왜 여기선 되고 저기선 안 되지?" 하는 상황을 만나게 됩니다.

자동 변환이 안 되는 경우

자동 변환은 플러그인이 콜백을 추적할 수 있을 때만 동작합니다. 다음 세 경우에는 'worklet'을 직접 붙여야 합니다.

경우이유
다른 파일에서 import한 함수플러그인이 다른 파일까지 추적하지 못함
표현식의 결과로 만들어진 함수어떤 함수가 선택될지 빌드 타임에 알 수 없음
커스텀 훅의 콜백커스텀 훅을 등록하는 API가 아직 없음

"When importing a function from another file or a module and using it as a worklet, you must manually add the 'worklet' directive to the function"
Worklets Babel Plugin

애니메이션이 이유 없이 동작하지 않는다면 이 세 가지 중 하나에 해당하는지 확인해보면 좋습니다.

런타임끼리는 어떻게 통신할까

앞에서 짚었듯 각 런타임은 자기만의 글로벌 스코프와 힙이 있습니다. 서로 메모리를 공유하지 않습니다.
UI Runtime에서 계산한 결과를 React 상태로 돌려보내거나 반대로 JS Thread의 값을 애니메이션에 반영하는 방법은 두 가지입니다.

함수 실행 예약: 스케줄링 API

특정 함수를 다른 런타임에서 실행하도록 예약하는 API입니다.

API방향
scheduleOnUI(fn, ...args)어디서든 → UI Runtime
scheduleOnRN(fn, ...args)Worklet Runtime → RN Runtime
scheduleOnRuntime(runtime, fn, ...args)어디서든 → 지정한 Worker Runtime
import { scheduleOnUI } from "react-native-worklets";
 
const logOnUI = (value: number) => {
  "worklet";
  console.log(value);
};
 
scheduleOnUI(logOnUI, 42);

UI Runtime에서는 React 상태를 직접 바꿀 수 없습니다. 그래서 scheduleOnRN으로 RN Runtime에 넘겨야 합니다.

import { scheduleOnRN } from "react-native-worklets";
 
// 컴포넌트 안: const [finished, setFinished] = useState(false);
 
function onAnimationEnd() {
  "worklet";
  // 여기는 UI Runtime — setFinished를 직접 호출할 수 없다
  scheduleOnRN(setFinished, true);
}

오래된 Reanimated 자료를 보면 runOnUI, runOnJS라는 이름이 자주 등장합니다. 이 API들은 현재 deprecated 상태이고 각각 scheduleOnUI, scheduleOnRN으로 대체됐습니다. 개념은 동일하니 예전 자료를 볼 때 이름만 다르다고 생각하면 됩니다. (runOnUI 문서)

값 공유: Shared Value

값을 계속 주고받아야 한다면 전용 메커니즘이 필요합니다. 앞의 useAnimatedStyle 예제에 나왔던 useSharedValue가 바로 그 역할을 합니다.

"useSharedValue lets you define shared values in your components."
useSharedValue

.value로 읽고 쓰면 런타임 경계를 넘어 값이 공유됩니다.

const x = useSharedValue(0);
 
// JS Thread에서 쓰기
x.value = 100;
 
// UI Runtime의 worklet에서 읽기
const animatedStyle = useAnimatedStyle(() => {
  return { transform: [{ translateX: x.value }] };
});

내부적으로는 Worklets의 저수준 API인 Shareable 위에 구현되어 있습니다.

"this technique is used to implement Reanimated's Shared Values API on top of Shareables."
createShareable

읽기 비용은 방향에 따라 다릅니다.

"When you read the sv.value on the JavaScript thread, the thread will get blocked until the value is fetched from the UI thread."
useSharedValue

JS Thread에서 .value를 읽으면 UI Thread와 동기화될 때까지 블로킹됩니다. 반복해서 읽으면 대기 시간이 그만큼 쌓입니다. 그래서 JS Thread에서 shared value를 자주 읽는 패턴은 피하는 게 좋습니다.

react-native-worklets는 이제 별도 패키지다

Reanimated 4부터 Worklet 관련 기능은 react-native-reanimated가 아니라 별도의 react-native-worklets 패키지로 분리됐습니다. 애니메이션이 아닌 다른 라이브러리에서도 Worklet을 쓸 수 있게 하기 위한 변경입니다.

"In Reanimated 4, worklets implementation has been moved to a separate library named react-native-worklets."
Migrating from Reanimated 3.x to 4.x

Babel 플러그인 경로도 react-native-reanimated/plugin에서 react-native-worklets/plugin으로 바뀌었습니다. 마이그레이션 시 참고하면 좋습니다.

정리

Worklet은 애니메이션·제스처처럼 프레임에 민감한 계산을 붐비는 JS Thread에서 떼어내 다른 JavaScript 런타임에서 실행하기 위한 짧은 함수입니다.

  • Thread와 Runtime은 별개입니다. Runtime은 JS 실행 환경, Thread는 OS 실행 단위이고 1:1로 묶여있지 않습니다.
  • 런타임은 RN RuntimeWorklet Runtime(UI / Worker)으로 나뉘고 각각 개수와 역할이 다릅니다.
  • 일반 함수가 런타임 경계를 넘으려면 workletize되어 직렬화 가능한 형태가 되어야 합니다.
  • 런타임끼리는 메모리를 공유하지 않으므로 스케줄링 API로 실행을 넘기거나 Shared Value로 값을 공유합니다.

다음 편에서는 Worklet이 캡처하는 클로저가 같은 런타임과 다른 런타임에서 왜 다르게 동작하는지 다룹니다.

참고 문서