[React 완벽가이드] Section 12 & Section 13

gonn-i·2024년 6월 18일
0

React 완벽 가이드

목록 보기
9/18
post-thumbnail

본 포스트는 Udemy 리액트 완벽가이드 2024 를 듣고 정리한 내용입니다.

Section 12

-> 다음에 코드 하나씩 뜯어서 정리할 예정

Section 13: 리액트와 최적화 테크닉

목차 🌳
1️⃣ 리액트 DOM의 업데이트 방법
2️⃣ 불필요한 업데이트(재랜더링) 막는 방법
3️⃣ Key에 대해서 알아보기 -> 리스트에서 꼭 필요
4️⃣ 다수의 State 설계 및 조율

리액트 DOM의 업데이트

당신 리액트 만져봤다면, 당연히 대답할 수 있어야 하는 질문!

🤔어떻게 컴포넌트 내용이 실행되는가?
🤔리액트는 사용자가 보는 화면을 어떻게 업데이트 하는가?

🤔어떻게 컴포넌트 내용이 실행되는가?

컴포넌트를 화면에 랜더링 한다
= 리액트가 컴포넌트 함수를 실행해서, return으로 jsx 반환한다.

(컴포넌트 실행 순서)
유일하게 main.jsx에 참조되어 Root객체에 랜더링되는 App컴포넌트가 제일 먼저 실행됨 (보통 따로 건들이지 않는 이상 App 컴포넌트)

main.jsx

import ReactDOM from 'react-dom/client';

import App from './App.jsx';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

App 컴포넌트가 가장 먼저 랜더링되는데, 이때 코드가 차례대로 읽히게 된다.

(최상위 컴포넌트) 컴포넌트 내부에 선언된 State + 함수 생성 (아직 실행 ❌) => JSX 코드 실행 (반환) => JSX 내부에 사용된 custom component에 접근 =>

(자식 컴포넌트 ) 컴포넌트 내부에 선언된 State + 함수 생성 (아직 실행 ❌) => JSX 코드 실행 (반환) => JSX 내부에 사용된 custom component에 접근

(자식 컴포넌트 ) ... 반복

컴포넌트 트리에 따라서 실행됨

컴포넌트 트리

React 실행 순서와 업데이트 방식

컴포넌트 함수 실행 -> jsx 코드 반환 -> 컴포넌트 트리 형성 -> 가상 DOM에 삽입 -> 가상 DOM 트리를 바탕으로 실제 DOM을 생성 -> [if, 변경 사항 발생] -> New version 가상돔 생성 -> 이전 version과 비교하여 다른 점만을 찾아냄 -> 변경된 부분만 실제 DOM에 반영


🤔리액트는 사용자가 보는 화면을 어떻게 업데이트 하는가?

virtual DOM 을 사용해 변하는 부분만 빠르게 수정해서 브라우저 보여줌

여기에서 잠깐 알고 있었지만, 한번만 더 집고 넘어가는 React의 특징!

DOM 이 뭔데?

(모른다면 반성)
그치만 면접에서 조리있게 말하려고, 계속 반복하면 을매나 좋게요 ~

DOM (Document Object Model)이란
하나의 웹 페이지 (= Document)에서 HTML 문서를 계층 구조로 표현한 객체 모델
구조: Document 노드 > Element 노드 > Text 노드 / Attribute 노드 ..
역할: DOM 트리를 통해 웹 페이지를 동적으로 조작할 수 있음

virtual DOM 은 뭔데?

(이거 모르면 진짜 반성) ~

Virtual DOM 이란
가상 메모리에 존재하는 DOM 트리의 복제본

동작 방식(DOM 업데이트 방법):
Virtual DOM 트리 생성 > 변경 사항 비교 > 실제 DOM 업데이트

Virtual DOM 트리 생성 : 상태가 변경되면, 새로운 버전의 가상 돔 트리 생성!!
변경 사항 비교 : new 트리와 이전 트리 비교하여, 변경된 부분만 찾아냄 (diffing 알고리즘)
실제 DOM 업데이트: 이로써, 변경된 부분만 실제 DOM에 적용 (전체 DOM을 다시 랜더링하는 것보다 훨씬!!! 효율적)

무튼 그래서 이런 가상돔을 써서, 더 빠르다 ~


Devtools Profiler 로 함수 실행 분석하는 방법 🧐

어떤 컴포넌트가 재실행되었고 아닌지를 볼 수 있다.

devtools에서 profiler를 켜고,
파란버튼을 누르면 측정이 시작되는데 이때 웹에서 Increment를 누르고 다시 파란 버튼 자리를 누르면 위와 같이 측정 결과가 나온다.

(불꽃 버튼 누르면)
이때, 색깔이 드러난 막대들은 재랜더링된 컴포넌트들을 나타낸 것이다. (색이 없는 것들은 재실행되지 않은 것)
또, 위에 있을 수록 부모 요소이며, 계층을 보여준다.

(막대 버튼 누르면)
재랜더링된 컴포넌트만 보여줌.

-> Profiler 를 통해, 어떻게 컴포넌트가 재랜더링 되었고, 왜 다시 실행되었는지를 알 수 있다.


불필요한 업데이트(재랜더링) 막는 방법 🤲

상위 컴포넌트의 상태가 업데이트 되면,
자동적으로 중첩 컴포넌트도 모두 실행이 되기 때문에 다시 랜더링될 필요가 없는 코드들까지
다시 실행되기 때문에 이는 비효율을 야기한다.

memo 함수를 사용 🪽

자식 컴포넌트에 memo 함수를 사용한다.
memo 는 뭘 하는데요?
컴포넌트 함수의 props를 살펴, 부모로부터 이전에 전달받은 prop값새롭게 전달받은 prop값을 비교하여 같으면 memo 가 해당 컴포넌트의 재실행을 저지함!
(부모로 부터 전달받은 props가 다르거나 + 내부적으로 변경사항이 있다면, 당연히 재실행됨 )

자식 컴포넌트

const Counter = memo(function Counter({ initialCount }) {
 
  //... 생략

  return (
    <section className="counter">
  	//... 생략
      <p>
        <IconButton icon={MinusIcon} onClick={handleDecrement}>
          Decrement
        </IconButton>
        <CounterOutput value={counter} />
        <IconButton icon={PlusIcon} onClick={handleIncrement}>
          Increment
        </IconButton>
      </p>
    </section>
  );
})

이러면 App -> Counter -> IconButton / CounterOutpu / IconButton
으로 실행되던 component 트리가 app component 에서 동일한 propsCounter component 전달했다면, memo가 Counter의 재실행을 막아주어

App -> 🛡️🛡️ Counter -> IconButton / CounterOutpu / IconButton

결국! App 컴포넌트만 재실행된다 (물론, 다른내용의 props의 전달, Counter 내부의 상태 변경 제외 )

memo 너무 많이 사용하면 🙅🏻‍♀️ 👎
1️⃣ 가능한 상위 컴포넌트에 올려서 사용할 것 (하위에 여러개 넣는거 보단 위에서 한번에 막아주기)
2️⃣ memo가 props 값을 비교해보는 과정이 늘어나면 그만큼 성능에 부담

현명한component 구성 🪽

현명한component 구성

부모에서 너무 많은 상태 관리로 잦은 재랜더링을 방지하고,
효율적이고 현명하게 component 분리하자!

함수 메모이제이션으로, 불필요한 재실행을 막는 방법 useCallback

memo로 자식 컴포넌트의 재실행을 막았다면,
useCallback으로 함수를 메모이제이션하여! 불필요한 재랜더링을 막을 수 있다.

useCallback: 함수의 인스턴스를 메모이제이션하여,
의존성 배열의 값이 변경되지 않으면 동일한 함수 인스턴스를 반환!
dep가 바뀌면, 형태는 이전과 같지만 른 메모리 주소를 가진 새로운 함수를 반환!!

사용방법:

const 함수명 = useCallback( 함수, [ 의존성 ])

값을 메모이제이션하여, 불필요한 함수의 재실행을 막는 방법 useMemo

useMemo: 값을 메모이제이션하여!
의존성 배열의 값이 변경되지 않으면 메모이제이션된 값을 그대로 반환

사용방법:

useMemo(() => 함수, [ 의존성 ])

useCallback vs useMemo vs Memo ⭐️

useCallback : 계산된 값의 메모이제이션
-> 자식 컴포넌트에 전달되는 콜백 함수의 재생성을 방지하여, 불필요한 재렌더링을 방지
useMemo : 함수의 메모이제이션
-> 계산 비용이 높은 연산의 결과를 메모이제이션할 때 사용
Memo : 컴포넌트의 메모이제이션
-> 동일한 props로 렌더링될 때 컴포넌트를 재사용하여 불필요한 재렌더링을 방지

State 관리와 key의 역할!

1) list 나열에서의 key의 중요성!✨

map함수를 통해, 나열된 리스트 컴포넌트들을 독자적으로 관리하고, 업데이트를 효율적으로 관리하게 해줌!

리액트의 재사용성이 좋을 수 밖에 없는 이유
각 컴포넌트 별로 독립적인 상태관리가 가능하기 때문에

	<Counter initialCount={chosenCount} />
	<Counter initialCount={chosenCount} />

이렇게 나란히 커스텀 컴포넌트를 배치했다고 가정할때,

두 컴포넌트는 독립적으로 chosenCount를 관리하고 있음!! (동기화 ❌)

반면, map으로 생성되는 컴포넌트들은 key를 통해 독립성 부여주어야 함.

``jsx

    {history.map((count, index) => ( ))}
```

다음과 같이 map 함수를 통해, list 형제 요소를 뽑아낼때에 우리는 고유성을 보장을 위해 key prop을 부여해준다. 하지만, map 함수에서 파생된 index 를 넘겨줄때에는 몇몇 문제가 발생한다.

index 사용시 문제점
1️⃣ 배열이 변경될 때마다 key 값이 변경
-> index배열 내에서 요소들의 위치를 나타내는 값으로,
배열에 요소가 추가되거나 배열이 변경되면 요소의 위치도 변경될 수 있음
(따라서, 배열의 업데이트에 따라서, 이전과 다른 위치의 요소를 동일한 key로 인식)
2️⃣ key 를 사용해, 이전과 새로운 요소를 효율적으로 비교하여 업데이트할 수 없음
-> index를 이용할때, 요소의 추가, 삭제 또는 재정렬시 모든 형제 요소가 같이 재랜더링됨 (바뀐 요소만 재랜더링 ❌ -> 전체 목록을 다시 새롭게 재랜더링 👎)

[index 사용으로 인해, 고유하게 컴포넌트를 구별 불가능]

해결방안
index는 위치를 기반으로 나와서 가변적이라 절대 쓰지말기!!
⭐️ 고유한 id를 부여하여, 각각의 요소를 식별가능하게 구분해주기!!!

[업데이트시, key값을 통해 추가되는 값만을 재랜더링]

2) key 를 통한 컴포넌트 초기화


function App() {

  const [chosenCount, setChosenCount] = useState(0);
  
  return (

		<Counter key={chosenCount} initialCount={chosenCount} />

  );
}

다음과 같이 key prop에 state 값을 넣어주면, state 가 업데이트 될때마다, 이전 상태의 컴포넌트가 지워지고 새로운 상태에 대한 컴포넌트가 다시 그려지게 된다!!!

물론 Counter 컴포넌트 안에서 useEffect 의 depchosenCount를 넣을 수도 있지만
잊지마라! useEffect는 컴포넌트를 완전히 다 읽어내고 추가적으로 작동시키는 부가적인 움직임이기 때문에 기본 랜더링 + a 라는 사실을!


state 스케줄링 & 배칭

  const [chosenCount, setChosenCount] = useState(0);

  function handleEnter(newCount) {
    setChosenCount(chosenCount + 1)    // --- 1번 
    setChosenCount((prevCount) => prevCount + 1) // --- 2번 
  }

위의 코드에서 1번과 2번의 차이점에 대해서 대답할 수 있는가?
물론 강의 초반 섹션에서 언급한바 있었지만 다시 복습하는 차원으로 다시 이번에 다루게 된 것 같은데 아무튼

둘중에 하나는 틀렸는데 그건 바로바로 1번
2번이 옳은 것이다!!

~~왜여? ~~
라고 묻는다면,

setChosenCount(chosenCount + 1) 을 실행하고, 바로 콘솔로 chosenCount를 찍어본다면 리액트는 바로 변화된 상태 값을 가져올 것이라고 생각하겠지만, 항상 매순간 최산 상태를 보여주진 않는다는 것이다.

왜냐하면 리액트가 state 업데이트의 스케줄을 조정하기 때문인데, 이때문에 즉각적으로 이전 상태를 휘리릭 가져오지 못한다.

따라서 이전 state를 기반으로, 새로운 state로의 업데이트를 하고 싶다면
setChosenCount((prevCount) => prevCount + 1) 와 같이 함수를 전달하여, 리액트로부터 보장받는 최신상태(state)를 이용하여야 한다.

만약 다음과 같이, chosenCount 을 새로운 입력값으로 바꾸고, 거기에 +1을 하고 싶다면?
아래와 같이 코드를 쓸 수 있는데 그러면, 컴포넌트도 2번 돌아가나여?

  const [chosenCount, setChosenCount] = useState(0);

  function handleEnter(newCount) {
    setChosenCount(newCount);
    setChosenCount((prev) => prev + 1);
  }

아뇽 🙂‍↔️ state 스케줄링에 따라서 여러 state 업데이트가 있고 동시에 실행되어야 한다면(같은 함수 안에서 같은 상태 변경이 일어났다면 실행되어야 한다면 -> 컴포넌트는 상태 변경이 n번되었다고 n번 재랜더링되는 것이 아니라 state batching 을 통해 위 컴포넌트는 1번만 재랜더링


0개의 댓글