JIGGAG

10월 한달동안 로그

2022년 10월 31일

읽어보았던

useMemo와 useCallback

  • [번역] useMemo 그리고 useCallback 이해하기
  • 리렌더링
    • 상태를 기반으로 DOM이 어떻게 그려졌는지에 대한 스냅샷
  • 이러한 스냅샷을 빠르게 만들어서 사용자에게 UI 업데이트가 반영된 버전을 전달하기 위해 리렌더링 최적화가 필요하다
    • 단순히 렌더링 과정에서 작업해야하는 양을 줄이거나
    • 다시 렌더링 되어야하는 횟수를 줄이는 방법이 있다
  • useMemo, useCallback 이용하기
    • 무겁고 시간이 오래 걸리는 연산을 다시 하지 않도록 캐싱된 데이터를 사용하는 것
    • useMemo를 이용해 매번 계산을 하지 않고 다시 사용할 수 있도록 처리할 수 있다
    • 리렌더링 되는 시점마다 새로운 상태 참조값을 반환하게 되고 이는 하위 컴포넌트를 전부 리렌더링 하게 된다
    • 컴포넌트에 memo를 사용하게 된다면 마치 순수 컴포넌트인 것 처럼 여겨지지만
    • 실제로는 메모이제이션 하지 않은 props를 전달 받는다면 (프리미티브 타입이라면 그나마 괜찮지만 그렇지 않은 경우)
    • memo가 되었더라도 참조값 변경된 props에 의해 컴포넌트 전체가 리렌더링 된다
    • 이것은 참조가 변경되지 않도록 children도 useMemo 해야하는 이유
    • useMemo이 값의 참조 캐싱을 위한 것이라면 useCallback 값 대신 함수
  • 이 글에서는 과한 useMemo, useCallback 사용을 적극 권하고 있지 않지만 개인적으로는 적극 권한다
    • 분명 이슈가 생기기 때문이다
    • 아마 이 글에서는 과하게 사용하기 보다 최적화 이슈가 발생했을때 해당 원인을 좀 더 정확하게 파악하면서 처리하기를 기대하는 의미인 것 같다

useEffect 완벽가이드

  • 코뿔소 JS 완벽 가이드처럼... useEffect 완벽가이드
  • 모든 랜더링은 고유의 Prop과 State가 있다
    • state를 업데이트할 때마다, 리액트는 컴포넌트를 호출합니다. 이 state값은 함수 안에 상수로 존재하는 값입니다
    • 리액트를 통해 전달된 값(state)은 컴포넌트가 다시 호출됨에 따라 DOM에 업데이트 되는 것
    • 각각의 렌더링마다 격리된 고유한 값이 바뀌는 것이다
    • 특정 랜더링 시 그 내부에서 props와 state는 영원히 같은 상태로 유지됩니다
    • 컴포넌트의 랜더링 안에 있는 모든 함수는 (이벤트 핸들러, 이펙트, 타임아웃이나 그 안에서 호출되는 API 등) 랜더(render)가 호출될 때 정의된 props와 state 값을 잡아둔다
  • 왜 useReducer가 Hooks의 치트 모드인가
    • 랜더링간 dispatch 의 동일성은 여전히 보장는 것을 이용하여
    • reducer를 컴포넌트 내부에서 정의하고 props에 접근하도록 하고 모든 랜더링마다 새로운 props를 바라보도록 정리
    • dispatch는 그냥 액션만 기억할뿐 새로운 스코프의 리듀서가 동작
  • 하지만 저는 이 함수를 이펙트 안에 넣을 수 없어요
    • 의존성을 추가하고 싶지 않은 경우
    • prop이나 state를 반드시 요구하지 않는 함수는 컴포넌트 바깥에 선언
    • 유틸 함수로 빼는 것과 동일
  • 함수도 데이터 흐름의 일부인가?
    • useCallback, useMemo 을 사용하면, 함수는 명백하게 데이터 흐름에 포함됩니다. 만약 함수의 입력값이 바뀌면 함수 자체가 바뀌고, 만약 그렇지 않다면 같은 함수로 남아있다고 말 할 수 있습니다
    • 콜백을 props로 내려보내는 것을 피하는 것이 더 좋다
    • 여러개의 콜백 props 보다 dispatch 전달하도록
    • 하지만 안티패턴을 자주 사용하고 있다
    • 그 예로 useCurring...

react-native-screens 🚨

  • v3.18.0이 나와서 드디어 fabric + android 조합을 쓸 수 있을까 기대했다
    • 문제의 안드로이드 이슈가 해결되어 보였는데
    • 아직 여전히 빌드는 실패하고 있다
  • 그럼에도 긍적적인!
    • 에러 메세지가 바뀌었다
    • 👍
      Execution failed for task ':app:mergeDebugNativeLibs'.
      > A failure occurred while executing com.android.build.gradle.internal.tasks.MergeNativeLibsTask$MergeNativeLibsTaskWorkAction
         > 2 files found with path 'lib/arm64-v8a/libc++_shared.so' from inputs:
            - /Users/jiggag/Projects/react-native-starter/node_modules/react-native/ReactAndroid/build/intermediates/library_jni/debug/jni/arm64-v8a/libc++_shared.so
            - /Users/jiggag/.gradle/caches/transforms-3/87b4b130d2c85494c51740eb879d8459/transformed/jetified-react-native-0.70.0-debug/jni/arm64-v8a/libc++_shared.so
           If you are using jniLibs and CMake IMPORTED targets, see
           https://developer.android.com/r/tools/jniLibs-vs-imported-targets
      
    • 무언가 패키지 설정을 정리해주어야만 할 것 같다 🤔

React Fiber

  • React Fiber 톱아보기
  • Fiber
    • Reconciliation 엔진 재구성
    • 애니메이션, 레이아웃, 제스처, 중단 또는 재사용 기능과 같은 영역에 대한 적합성을 높이고 다양한 유형의 업데이트에 우선 순위를 지정
    • 렌더링 작업을 여러 덩어리로 나누어 여러 프레임에 분산
  • Virtual DOM
    • Real DOM의 in-memory 표현
      • React에서는 Virtual DOM이란 UI를 나타내는 객체로 통용되며, React elements와 연관된다.
      • React는 컴포넌트 트리에 대한 추가 정보를 포함하기 위해 fibers라는 내부 객체를 사용한다.
    • reconciliation: 호출되는 렌더 함수에서 화면에 표시되는 요소가 되는 사이에 발생한 단계
    • Virtual DOM을 reconciliation 과정을 통해 React Elements를 나타낸다
  • Reconciliation
    • React가 변경해야 할 부분을 결정하기 위해 한 트리를 다른 트리와 비교하는 데 사용하는 알고리즘이다.
      • 앱에 업데이트를 반영하기 위해 앱 전체를 리렌더 하는 것은 비효율적이다
      • 따라서 Reconciliation 과정을 통해 최적화
    • Virtual DOM이 한다고 여겨지는 것의 알고리즘
      • 각각 노드 트리 컴포넌트의 type, key를 확인하여 업데이트 요청한다
      • 컴포넌트의 type, key 자체가 다르면 diffing 조차 하지 않고 트리를 완전히 교체한다
        • 상위 컴포넌트 type, key이 달라지면 트리가 아예 교체되어 버리니 하위 컴포넌트도 리렌더 해야한다
  • Reconciler !== Renderer
    • Reconciler은 트리의 어떤 부분이 변경됐는지 계산한다
    • Renderer은 계산된 정보를 앱을 실제로 업데이트하는 데 사용한다
    • React Core가 제공하는 동일한 Reconciler를 공유하면서 각자 자체적인 Renderer의 사용을 가능하게 한다.
      • 지난번에 보았던 React를 Reconciler 엔진을 통해 변환하면 Renderer가 그리는 내용!
  • 그렇다면 Fiber는 왜 나왔을까
    • Fiber는 Reconciliation 엔진 재구성한 버전이다
      • Fiber === Reconciliation가 하던 역할 + 최적화
    • 이미 Reconciliation는 diffing을 통해 업데이트를 진행하고 있는데, 여기서 좀 더 최적화를 진행한다면?
  • 🚨 모든 업데이트를 즉시 적용할 필요는 없다
    • 예를 들면, 스크린 전체가 업데이트가 되어야하지만 뷰포트 밖에 있는 것은 좀 더 나중에 업데이트 되어도 괜찮다
    • 이런 우선순위(=스케줄링)를 Fiber가 판단하여 최적화하는 것을 목표로 한다
  • 다시 Fiber
    • Fiber의 목표는 스케줄링을 통해 리액트 최적화 하는 것
      • 하던 일 멈추고 우선순위 높은 일 진행하고 다시 돌아와서 마무리하거나 필요하지 않은 일 정리하기 등
  • 리액트 컴포넌트를 그리는 것은 결국 콜스택에 함수를 호출하는 것인데
    • 이 콜스택을 스케줄링을 통해 제어한다면 최적화를 진행할 수 있게 된다
    • Fiber가 이 대단한 일을 하는 것
      • DOM을 재구성한 Virtual DOM
      • Stack을 재구성한 Virtual Stack
    • 메모리에 스택을 보관하고 원하는 시점에 실행
  • Fiber의 구조
    • type, key
      • 컴포넌트 스택과 재조정 시 판단하기 위함
    • child, sibling
      • 재귀적인 트리 구조로 다른 Fiber를 나타낸다
      • 여기서 child가 하위 트리라고 생각했는데, 현재 Fiber에 의해 렌더되는 반환값이였다 😱
      • 다시 생각해보니 렌더된 반환값이 child 인게 맞구나!
      • Fiber가 A컴포넌트를 그렸다면 child는 그 반환값인 B겠당
    • retur
      • Fiber가 반환해야하는 Fiber
      • Parent Fiber
    • pendingProps, memoizedProps
      • 함수 실행 시작 시 pendingProps, 끝에서 memoizedProps 설정
      • 이 두 값이 같다면 Fiber는 이전 결과를 재사용하여 불필요한 업데이트 방지
    • pendingWorkPriority
      • Fiber가 처리하려는 작업의 우선순위
    • alternate, output
      • alternate=flush이면 output을 화면에 렌더링 하는 것을 의미
      • 모든 Fiber가 output을 가지지만 트리 위로 전달되어 리프 노드에서만 생성되고
      • Fiber가 호출한 함수의 반환값으로 최종적으로 렌더러가 처리한다

Fabric이 가져올 효과

  • React Native Architecture — Old Vs New
  • 아래 React를 Reconciler가 해석해서 RN 이 그린다에 이어서
  • 그럼 RN이 그리는 방법은?
    • JS 스레드에서 브릿지를 통해 비동기로 Native, Main 스레드에서 처리하고 받는데
      • 직렬화된 json을 브릿지로 전달하고 해석하고 그리고..
    • JS 스레드에서 이미 완료된 이벤트에 대해서도 비동기로 동작하다보니 아직 되돌려받지 못한 경우
    • 백화로 그려지는 이슈를 마주하게 될 수 있다
  • 따라서 새로운 아키텍쳐인 Turbo module과 Fabric이 이걸 곧장 동기화 한다

Reconciler

  • 작년 feconf에서 봤었는데, 다시 톱아보기
  • 코드(js)를 엔진(v8)이 해석해서 cpu에게 전달하여 실행
    • cpu가 이해하는 어셈블리어는 모른채 js로만 작성
  • 리액트에서는?
  • reconciler
    • function, class components, props, state, effect, ref... 등 리액트 문법
    • 렌더러와 무관하게 리액트 코드를 해석하고 실행 (= 엔진)
  • react를 reconciler가 해석해서 호스트 환경(UI 렌더러)에 전달하여 실행
    • 다양한 호스트 환경에서 동일하게 사용할 수 있는 리액트 문법으로 작성
    • react-dom, react-native...
  • DOM을 직접 조작하지 않고 Virtual DOM을 통하는 이유
    • 직접 DOM을 제어하지 않는다는 것이 다양한 호스트 환경을 고려한 것
    • js를 실행하기만 할뿐 특정한 환경을 고려하지 않는다
    • 참고
    • 리액트 페키지들은 오직 당신이 리액트 특성들을 사용할수 있도록 해주지만 어떻게 사용될지에 대해서는 아무것도 모른다는 겁니다. 렌더러 페키지들은(리액트-돔, 리액트-네이티브, 등등) 리액트 특성들의 실행을 제공하고 특정-플렛폼의 논리를 제공합니다.
  • 대수적 효과?
    • 리액트가 마주한 문제를 해결하기 위해 사용한 답
    • 그리고 얻은 결과: context, suspense 🤔
    • 호출 스택(이 경우, 리액트)의 중간에 있는 함수들을 신경쓸 필요 없이, 혹은 그 함수들을 async나 제너레이터 함수로 강제로 변경할 필요 없이, 호출 스택의 아래쪽에 있는 코드가 윗쪽에 있는 코드에게 뭔가를 전달할 수 있다

FE 테스트

  • 테스트 가능한 프런트엔드: 좋은 것, 나쁜 것, 깨지기 쉬운 것
    • 한 코드를 테스트하려고 할 때, 우리는 종종 React 컴포넌트를 렌더링하고 결과를 테스트하는 React Testing Library와 같은 것에서부터 시작하거나, 프로젝트와 잘 작동하도록 Cypress를 구성하느라 정신없이 하다가 잘못된 구성으로 끝나거나 포기하는 경우가 많습니다.
  • 단위테스트는 모킹을 많이 하는 특정 환경에 의존하기 때문에 좋지 않다
    • ex. 로컬 상태는 A이고 useHook에서 가져오는 것은 B이고 props에서 C를 가져오는 환경에서 이 기능은 D 한다
    • 그렇다면 어떤 테스트를 해야할까?
  • 최대한 외부 의존성(redux, context)이 없는 컴포넌트로 분리하고 특성에 따라 단위, 통합, E2E 테스트
  • 관심사 분리를 통해 테스트를 쉽게 진행할 수 있다
    • 관심사 분리를 하면 단위 테스트 또는 UI 컴포넌트 테스트만 진행하면 되겠지만
    • 이런 분리가 쉽지 않은 서로 얽혀있는 경우에는 유일한 테스트 방법은 E2E 테스트로 모든 것을 테스트 하는 것