React Native 애플리케이션에서는 크게 두 가지 Thread가 중요한 역할을 합니다: JavaScript Thread와 Main Thread (UI Thread, Base Thread) (사실은 3가지 Thread).
대부분의 JavaScript 로직은 JavaScript Thread에서 실행되며, UI 업데이트는 UI Thread 에서 처리됩니다. React Native Reanimated 라이브러리는 이 두 Thread 간의 효율적인 상호작용을 통해 부드러운 애니메이션과 제스처 처리를 가능하게 합니다.
React.useCallback은 React 훅 중 하나로, 주어진 콜백 함수를 메모이제이션함으로써 컴포넌트가 리렌더링될 때 해당 함수를 재생성하지 않고 재사용할 수 있도록 도와줍니다. 이 훅은 함수를 정의할 때 사용되며, 정의된 콜백 함수는 기본적으로 JavaScript Thread에서 실행됩니다.
예를 들어, 사용자 인터랙션에 응답하거나 데이터를 가져오는 등의 작업을 수행하는 함수를 React.useCallback을 사용하여 정의했다면, 이 함수는 JavaScript Thread에서 호출되고 실행됩니다. 이 경우, 해당 함수의 실행 컨텍스트는 이미 JavaScript Thread이기 때문에, UI Thread로의 전환을 고려할 필요가 없습니다.
Reanimated 라이브러리에서는 worklet이라는 개념을 사용하여 UI Thread에서 직접 실행될 수 있는 작은 코드 블록을 정의합니다. 이러한 worklet 함수는 애니메이션 또는 제스처 처리 로직을 UI Thread에서 실행하여 성능을 최적화합니다. 그러나 때때로, UI Thread(또는 worklet)에서 실행되는 로직이 JavaScript Thread에서 실행되어야 하는 함수를 호출해야 할 수도 있습니다. 예를 들어, 애니메이션의 상태를 기반으로 React 상태를 업데이트해야 할 때가 이에 해당합니다.
이때 runOnJS 함수가 사용됩니다. runOnJS는 Reanimated worklet에서 avaScript Thread로 함수 호출을 명시적으로 전환할 수 있도록 도와줍니다. 이는 worklet (UI Thread)에서 실행되는 코드가 JavaScript Thread에서 실행되어야 하는 로직을 안전하게 호출할 수 있게 해줍니다.
위 규칙을 지키지 않고 JavaScript 로직을 UI Thread에서 처리하면 아래와 같은 오류를 만난다.
ERROR ReanimatedError: [Reanimated] Tried to synchronously call a non-worklet function on the UI thread.
See `https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#tried-to-synchronously-call-a-non-worklet-function-on-the-ui-thread` for more details., js engine: reanimated
Error: ENOENT: no such file or directory, open '/Users/apple/Project/thessencard/installValueUnpacker'
at Object.readFileSync (node:fs:453:20)
at getCodeFrame (/Users/apple/Project/thessencard/node_modules/metro/src/Server.js:879:18)
at Server._symbolicate (/Users/apple/Project/thessencard/node_modules/metro/src/Server.js:962:22)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Server._processRequest (/Users/apple/Project/thessencard/node_modules/metro/src/Server.js:418:7) {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/apple/Project/thessencard/installValueUnpacker'
}
따라서, 해당 기능이 필요했던 부분을 구현해보면.
<Container onLayout={onContainerLayout}>
<GestureDetector gesture={tapGesture}>
<NativeContainer/>
</GestureDetector>
</Container>
아래 두 가지 방법으로 구현해 볼 수 있다.
const tapGesture = Gesture.Native().onTouchesUp(
React.useCallback(() => {
실행구문
}, []),
);
핵심은 콜백 함수는 기본적으로 JavaScript Thread에서 실행, 따라서 UI Thread로의 전환을 고려할 필요가 없음.
const tapGesture = Gesture.Native().onTouchesUp(() => {
'worklet';
runOnJS(실행구문);
});
핵심은 worklet (UI Thread)에서 실행되는 코드가 JavaScript Thread에서 실행되어야 하는 로직을 안전하게 호출할 수 있게 해줌.
여기서 궁금한점.
worklet은 없어도 아무 문제 없던데 빼도 될까?
worklet은 React Native Reanimated 라이브러리에서 사용되는 지시어며, 특정 함수를 worklet으로 표시하는 데 사용됩니다. worklet은 Reanimated에서 UI Thread(또는 네이티브 Thread)에서 직접 실행될 수 있도록 설계된 작은 JavaScript 함수입니다. 이러한 함수는 애니메이션 또는 제스처 처리와 같은 고성능이 필요한 작업을 위해 최적화되어 있으며, UI Thread에서 직접 실행되어 앱의 반응성과 성능을 향상시킵니다.
왜
worklet없이도 동작하는가?
자동 변환: 최신 버전의 Reanimated 라이브러리(특히 Reanimated 2 이상)는 특정 패턴을 자동으로 감지하여, 일부 함수를 worklet으로 자동 변환할 수 있습니다. 따라서, 개발자가 명시적으로 worklet을 추가하지 않아도, Reanimated가 함수를 worklet으로 인식하고 올바르게 처리할 수 있는 경우가 있습니다.
컨텍스트: onTouchesUp 같은 이벤트 핸들러 내부에서 작성된 코드는 이미 UI Thread에서 실행될 것으로 예상되는 컨텍스트에서 사용되기 때문에, Reanimated는 이러한 함수를 worklet 컨텍스트에서 실행하는 것으로 가정할 수 있습니다. 즉, 이벤트 핸들러 내부에서는 이미 worklet 컨텍스트 내에서 실행되고 있다고 판단할 수 있어 worklet 지시어가 생략되어도 동작할 수 있습니다.
runOnJS의 역할: runOnJS 함수는 worklet에서 실행되는 코드가 JavaScript Thread에서 실행되어야 할 때 사용됩니다. runOnJS가 호출되는 순간, Reanimated 라이브러리는 해당 호출이 worklet에서 발생했음을 인식하고, 자동으로 JavaScript Thread로 전환하여 함수를 실행합니다. 따라서, runOnJS를 사용하는 경우는 이미 worklet 컨텍스트 내에서 실행되고 있다는 간접적인 표시가 될 수 있습니다.
그러나, 명시적으로 worklet을 추가하는 것은 좋은 습관입니다. 모든 환경이나 상황에서 자동 변환 메커니즘이 예상대로 작동한다고 보장할 수 없으며, Reanimated 라이브러리의 향후 업데이트에서 내부 동작 방식이 변경될 가능성도 있습니다.
따라서, 함수를 worklet으로 명확히 표시하는 것은 코드의 명시성을 높이고, 다른 개발자가 코드를 이해하기 쉽게 만들며, 잠재적인 오류를 예방하는 데 도움이 됩니다.
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import androidx.appcompat.app.AppCompatActivity;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class MainActivity extends AppCompatActivity {
private ExecutorService executorService = Executors.newSingleThreadExecutor();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 백그라운드 Thread에서 실행할 작업
Runnable backgroundTask = () -> {
// 시간이 오래 걸리는 작업을 여기서 수행합니다.
// 예: 네트워크 요청, 데이터베이스 쿼리 등
String result = "Fetched Data";
// 작업이 완료되면, 메인 Thread로 결과를 전달합니다.
new Handler(Looper.getMainLooper()).post(() -> {
// 메인 Thread에서 UI를 업데이트합니다.
// 예: textView.setText(result);
});
};
// Executor를 사용하여 백그라운드 작업을 실행합니다.
executorService.execute(backgroundTask);
}
@Override
protected void onDestroy() {
super.onDestroy();
// Activity가 파괴될 때 Executor를 종료합니다.
executorService.shutdown();
}
}
핵심은 백그라운드 Thread에서 작업 할 때 UI 업데이트가 이루어 진다면 오류 발생(CalledFromWrongThreadException),
UI 업데이트는 Main Thread로 결과를 전달해야함.
runOnUiThread를 사용할 수도 있음. runOnUiThread는 Activity 클래스에 정의된 메서드로, 주어진 Runnable을 UI Thread에서 실행하도록 예약합니다. 이 메서드는 현재 Thread가 UI Thread인지 확인하고, 만약 그렇다면 즉시 Runnable을 실행합니다. 그렇지 않다면, Runnable을 UI Thread의 이벤트 큐에 추가하여 나중에 실행되도록 합니다.
컨텍스트 의존성: runOnUiThread는 Activity 메서드이므로 Activity 컨텍스트에서만 사용할 수 있습니다. 반면, Handler(Looper.getMainLooper())는 어떤 컨텍스트에서든 사용할 수 있습니다.
사용 편의성: 직접적인 UI 업데이트가 필요한 경우 Activity 내부에서 runOnUiThread를 사용하는 것이 더 직관적일 수 있습니다. 그러나 더 일반적인 목적이나 Activity 외부에서는 Handler가 더 유연한 솔루션을 제공합니다.
React-Native에서 onPress를 걸면 네이티브 컴포넌트에서 감지를 못하고,
네이티브 컴포넌트에서 클릭 됐다는걸 감지하는 방법을 알지 못하여 클릭 콜백함수를 심지 못하고 있었으나,아래 내용 추가하여 해결하였음.
앞서 기재했듯이 React Native에서는 JavaScript Thread와 네이티브 Thread가 별도로 존재합니다. 이벤트 처리도 이 두 Thread 간의 상호작용을 통해 이루어집니다. 기본적으로, React Native의 onPress와 같은 제스처 이벤트는 JavaScript Thread에서 처리됩니다. 이는 터치 이벤트가 발생하면 네이티브 Thread에서 JavaScript Thread로 이벤트 정보가 전달되고, 그곳에서 로직이 실행되는 방식입니다.
네이티브 컴포넌트는 직접적으로 React Native의 JavaScript 엔진과 통신하지 않습니다. 따라서, 네이티브 컴포넌트에서 발생하는 이벤트를 JavaScript Thread에서 직접 처리하기 위해서는 이벤트를 JavaScript로 명시적으로 전달하는 메커니즘이 필요합니다. 그러나 특정 상황에서는 JavaScript 레벨의 이벤트 처리만으로는 충분하지 않거나, 네이티브 이벤트 처리가 더 적합할 수 있습니다.
React Native Gesture Handler는 이러한 한계를 극복하기 위해 만들어졌습니다. 이 라이브러리는 네이티브 Thread에서 직접 제스처를 처리함으로써, 보다 부드럽고 일관된 제스처 인식을 가능하게 합니다. 이는 네이티브와 JavaScript 간의 통신 오버헤드를 줄이고, 특히 복잡한 제스처 인식이나 연속적인 인터랙션에서 성능을 향상시킵니다.
GestureDetector: 제스처를 감지하는 컨테이너 역할을 합니다. 이를 통해 다양한 제스처(탭, 스와이프, 핀치 등)를 감지하고, 해당 제스처에 맞는 핸들러를 실행할 수 있습니다.
Gesture.Native().onTouchesUp: 사용자가 화면에서 손을 떼었을 때 실행되는 이벤트 핸들러입니다. onTouchesUp은 특정 액션이 완료되었음을 감지하는 데 사용됩니다. 예를 들어, 사용자가 버튼을 누르고 손을 떼었을 때의 액션을 처리하는 데 적합합니다.
@Override
public boolean onTouchEvent(MotionEvent event) {
if (myCondition) {
// 특정 조건에서만 이벤트 처리
return true; // 이벤트 소비
}
return false; // 이벤트를 부모 뷰로 전달
}
Android에서 자식 뷰가 터치 이벤트를 소비해 버리면 부모 뷰에서는 그 이벤트를 감지할 수 없는 상황이 발생할 수 있습니다. 이는 자식 뷰의 onTouchEvent 메서드가 터치 이벤트를 처리하고 true를 반환하여 이벤트가 여기서 종료되었음을 나타내기 때문입니다. 이 경우, 부모 뷰는 자식 뷰에 의해 이미 처리된 터치 이벤트를 받지 못합니다.
이러한 상황을 극복하기 위해, Android는 GestureDetector와 같은 추가적인 메커니즘을 제공하여 복잡한 제스처를 감지하고 처리할 수 있도록 합니다. 부모 뷰가 GestureDetector를 사용하여 특정 제스처(예: 스와이프, 롱 프레스 등)를 감지하고자 할 때, 이를 통해 자식 뷰가 터치 이벤트를 소비하더라도 원하는 제스처를 인식하고 반응할 수 있습니다.
부모 뷰에서 GestureDetector를 사용하여 자식 뷰가 터치 이벤트를 소비하는 상황에서도 특정 제스처를 감지하고자 한다면, 부모 뷰의 onTouchEvent 또는 onInterceptTouchEvent 메서드 내에서 GestureDetector를 활용할 수 있습니다.
public class ParentView extends ViewGroup {
private GestureDetector gestureDetector;
public ParentView(Context context) {
super(context);
init(context);
}
private void init(Context context) {
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 여기에서 원하는 제스처를 처리
return true;
}
// 필요한 다른 제스처 메서드를 오버라이드
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// GestureDetector로 제스처 감지 시도
return gestureDetector.onTouchEvent(ev) || super.onInterceptTouchEvent(ev);
}
}
따라서, 자식 뷰에서 터치 이벤트를 소비해 버려 부모 뷰에서 이벤트를 감지하지 못하는 상황이 발생하더라도, GestureDetector를 활용하여 이 문제를 극복하고 원하는 제스처를 감지할 수 있습니다.