React Native의 동작원리와 애니메이션

최근 회사에서 Skeleton UI, 제스처 반응 모달, 반응형 헤더, 아코디언 리스트 등등…
여러 종류의 RN 애니메이션 관련 이슈를 할당받아, 애니메이션과의 씨름을 했었다…😅
애니메이션 관련 작업을 계속 하다보니, 코어한 부분에 대해 궁금증이 생기기 시작했고, 관련된 내용을 제대로 정리해보고자 RN 애니메이션 관련 블로그 포스팅을 계획하게 되었다.

React-Native의 동작 원리

우선, RN에는 메인 스레드와 JS스레드가 공존한다.

메인스레드에는 UI스레드가 존재하는데, 이 UI스레드는 말 그대로 UI를 생성하고 렌더링하는 것을 담당한다. 그리고 Shadow스레드는 백그라운드 스레드로써, RN이 엘리먼트의 레이아웃을 미리 계산하는 것을 담당하는 스레드다 (Yoga라는 레이아웃 엔진 사용). 마지막으로 JS스레드는 우리가 익히 알듯, 자바스크립트 엔진을 통해 자바스크립트 코드를 읽고 실행하는 역할을 담당한다.

Bridge

위의 사진처럼, 두 쓰레드는 서로 메시지와 응답을 주고 받기 위해 브릿지(Native Bridge)를 통해 통신한다. 하지만 여기서 반드시 짚고 넘어가야 할 부분은, 이 브릿지 사이에서의 통신이 간소화되면 간소화될 수록 Native App 자체의 성능이 향상될 수 있다는 것이다. 이는 마치 서버와 클라이언트 사이에 요청과 응답 횟수를 최소화 하면 할수록 서버 부하를 줄이며 효율성의 향상을 기대할 수 있다는 점과 유사하다고 볼 수 있다.

동작 원리 예시

원론적인 설명만을 통해서 플로우를 이해하기란 쉽지 않은 것이 사실이다.
코드 예시를 통해 조금 더 자세히 이해할 수 있도록 해보자!

우선, 아래와 같이 앱 화면 중앙에 파란색 점을 그리고자 의도했다고 가정해보자.

1
2
3
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<View style={{ width: 50, height: 50, backgroundColor: "blue" }}></View>
</View>

우리의 OS는 자신만의 레이아웃 시스템을 보유하고 있기 때문에, 위와 같은 flex, backgroundColor와 같은 CSS문을 이해하지 못한다. 따라서 RN이 OS가 해당 스타일문을 이해할 수 있도록 코드를 변환해줘야 한다.

이러한 이유 때문에 우리의 앱이 실행되면, 우리가 화면에 그리고자 하는 해당 엘리먼트들의 레이아웃 계산을 위해 Shadow스레드에 해당 작업이 할당되는 것이다.

근데 어떻게 JS스레드에서 작성된 위의 코드들이 Shadow스레드로 할당될 수 있는 것일까?🥸

바로 위에서 설명한 “브릿지” 덕분이다. 브릿지(Bridge)는 전달하고자 하는 데이터를 JSON데이터로 serialize(직렬화)하는데, 이를 통해 각 스레드 간에 데이터가 String 형태로 전달되는 것이다!

이제 그 뒤로 JS스레드에서 전달된 작업이 Shadow스레드에서 Yoga엔진을 통해 수행되어 레이아웃된 마크업이 렌더링 된다. 그리고 최종적으로 해당 작업내용이 메인스레드로 전달되어 화면에 최종적으로 렌더링되는 것이다.

정리 및 요약

  1. 앱 실행
  2. 메인 스레드 실행
  3. JS 번들링 → JS스레드로 작업이 이동(JS와 UI는 이제 비동기적으로 작업이 진행)
  4. 리액트와 JS가 변경사항을 Shadow 스레드에 전달
  5. Shadow 스레드가 레이아웃 관련 작업을 수행
  6. 메인 스레드로 전송
  7. 메인 스레드는 전달 받은 레이아웃을 화면에 렌더링

그러면 이제는 해당 동작 프로세스에 대한 이해를 기반으로,
RN에서의 애니메이션 동작 방법 및 활용방법에 대해 알아보도록 하자.


애니메이션 기초

부드러운 애니메이션을 구현하기 위해선, 1초에 60프레임의 표현이 되어야 하고, 이는 곧 16ms 내에 1프레임이 생성되어야함을 뜻한다.

하지만 RN에서 기본적으로 제공되는 애니메이션 동작 방식은 위와 같이, UI스레드와 JS스레드 간의 통신에 의해 이루어진다. 그리고 이 두 스레드 간의 통신은 비동기적으로 실행되기에, 1프레임/16ms라는 성능적 기준을 반드시 보장할 수 없다(JS에서의 비동기 통신은 이벤트루프에 의존하기 때문).

따라서, 기본적인 애니메이션 구현 방식은 성능의 최적화를 기대할 수 없기에, 우리는 reanimated의 사용을 충분히 고려해볼만 하다.

UI Thread만을 사용하는 Reanimated

제목에서도 바로 알 수 있듯, reanimated는 UI스레드만을 사용하기에, 우리는 굉장한 성능적 이점과 향상을 기대할 수 있다.

reanimated 라이브러리는 애니메이션과 관련된 로직을 UI 스레드에서만 실행하기 때문에, JS와의 비동기적 통신에서 올 수 있는 병목현상이나 효율성 저하의 우려를 떨쳐낼 수 있다는 장점을 갖는다. 즉, reanimated를 사용하면 프레임드랍이 없는 성능 좋고 부드러운 애니메이션의 실행을 기대할 수 있는 것이다.


Reanimated의 핵심

Reanimated를 활용하는 데 있어 가장 중요한 것은,
worklet에 대한 개념과, useSharedValue, useAnimatedStyle이라고 생각한다.

worklet

worklet은 UI스레드 환경에서 실행되는 하나의 함수다. 특이한 점은, UI스레드에서 동작함에도 불구하고 JS스레드로부터 인자를 넘겨받을 수 있고, JS스레드에 존재하는 상수에도 접근할 수 있다는 점이다.

조금 더 쉽게 설명하자면, 우리는 JS 함수가 JS스레드가 아닌 UI스레드에서 컨텍스트를 생성하여 실행할 수 있도록 하기 위해 worklet을 활용한다고 이해하면 된다. (UI스레드에서 실행될 수 있는 JS함수 코드!)

1
2
3
4
5
6
7
// 기본적인 사용 템플릿은 아래와 같으며, 함수코드 첫 줄에 'worklet'을 작성해주기만 하면 된다.
const width = 180;

function someWorklet() {
"worklet"; // 이 함수는 이제 별개의 컴파일 과정을 거친다. (worklet 때문)
console.log("width is", width);
}

useSharedValue

useSharedValue()를 통해 우리는 UI스레드와 JS스레드에서 공유할 수 있는 값을 생성하여 활용할 수 있다.

상술했듯, UI스레드와 JS스레드는 서로 병렬적이다. 우리는 이 값을 통해 애니메이션을 실행할 때 활용할 수 있다. 하지만 여기서 주의해야 할 점은, reanimated에서의 애니메이션 관련 로직은 UI스레드에서 거의 일어난다는 점이다. 이 때문에 sharedValue는 UI스레드에서 활용될 수 있도록 최적화되어 있고, UI스레드 내에서는 동기적으로 실행되고 동작한다.

하지만 이와 반대로, JS스레드에서 sharedValue의 업데이트는 비동기적으로 일어난다. 즉, 리액트에서의 setState가 비동기적으로 동작하듯, sharedValue 또한 그렇다고 이해하면 된다.

1
2
3
4
5
6
7
const SomeComponenet = () => {
const sharedValue = useSharedValue(100);

return (
<Button onPress={() => (sharedValue.value = 0)} /> // ".value"를 통해 접근
);
};

useAnimatedStyle

애니메이션 스타일을 관리하는 hook이다.

우리는 이 useAnimatedStyle을 통해 sharedValue에 따른 애니메이션을 구현할 수 있는 것이다. animationStylesharedValue값이 업데이트 될 때마다 함께 업데이트 된다는 특징을 갖는다. 이는 useEffect와 유사하다고 생각할 수도 있는데, 유용하게도 useEffect와 같이 [dependency]를 활용해 특정 업데이트 조건을 구현할 수도 있다.


활용 예제

그러면 간단한 예제를 통해 reaminated로 “박스 이동하기”를 구현해보도록 하자.

아래와 같은 빨간색 박스가 있다고 가정해보자.

1
2
3
<View style={{ flex: 1, justifyContent: "center" }}>
<Button style={{ width: 50, height: 50, color: "red" }} onPress={/* later... */} />
</View>

해당 박스를 누르면, 오른쪽으로 100만큼 이동시키는 애니메이션을 구현하려면 아래와 같이 useSharedValueuseAnimationStyle을 사용할 수 있을 것이다.

1
2
3
4
5
6
7
const offset = useSharedValue(0); // 초기값은 0

const animationStyle = useAnimationStyle(() => {
return {
transform: [{ translateX: offset.value }],
};
});

위와 같이 offset과 animationStyle에 대한 코드를 작성해주고, 이제 해당 값들을 우리의 컴포넌트와 로직에 대입해주기만 하면 된다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const offset = useSharedValue(0);

const animationStyle = useAnimationStyle(() => {
return {
transform: [{ translateX: offset.value }],
};
});

return (
<View style={{ flex: 1, justifyContent: "center" }}>
<Button
style={[{ width: 50, height: 50, color: "red" }, animationStyle]}
onPress={() => (!offset.value ? 150 : 0)} // 0일땐 150으로, 그 외에는 0으로 (토글)
/>
</View>
);

그리고 만약 정말 애니메이션과 같이 트랜지션 효과를 주고싶다면 animationStyle을 아래와 같이 수정해주기만 하면 된다.

1
2
3
4
5
6
7
8
const animationStyle = useAnimationStyle(() => {
return {
transform: [
// 300ms동안 값의 변화가 점진적으로 일어나고, 100초 뒤에 실행되는 딜레이 옵션을 추가 가능!
{ translateX: withTiming(offset.value, { duration: 300, delay: 100) }
],
}
})

물론, 위와 같은 애니메이션 외에도 복잡한 애니메이션의 경우에는 더 디테일한 로직이나 제스처 핸들링, 또는 커스터마이징 애니메이션이 들어갈 수 있다. 관련된 심화버전에 대한 내용은 추후에 다시 포스팅할 수 있도록 해보겠다 😊


참조한 자료

How React Native constructs app layouts (and how Fabric is about to change it)

원리와 예제를 통해 React-native-reanimated V2 입문하기


React Native의 동작원리와 애니메이션

https://hoonjoo-park.github.io/native/reanimated/

Author

Hoonjoo

Posted on

2022-07-17

Updated on

2022-07-17

Licensed under

Comments