1. 리액트 렌더링의 순서
아래 코드에서 4가지 console.log() 문이 어떻게 출력될지 예상해보자.
import React, { useState } from 'react';
import Login from './components/Login/Login';
import Home from './components/Home/Home';
import MainHeader from './components/MainHeader/MainHeader';
import { useEffect } from 'react';
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
console.log("in function")
useEffect(() => {
const storedUserLoggedIn = localStorage.getItem('isLoggedIn')
setTimeout(() => {
if (storedUserLoggedIn === '1') {
setIsLoggedIn(true);
}
console.log("in effect")
}, 3000)
},[])
const loginHandler = (email, password) => {
localStorage.setItem('isLoggedIn','1'); //1 means logged in.
setIsLoggedIn(true);
};
const logoutHandler = () => {
setIsLoggedIn(false);
};
return (
<React.Fragment>
<MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
<main>
{console.log("in return")}
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</React.Fragment>
);
}
export default App;
console.log 실행 순서는 다음과 같다.
- in function
- in return
- setTimeout (3초 후)
- in effect
- in function
- in return
앱 실행 시작 시점에 App 컴포넌트가 평가되면서 in function, in return 실행 ( +child component rerendring)
→ return 문까지 평가되고 effect 실행 (setTimeout 함수 실행)
→ useEffect 함수 내의 state 가 변경되었으므로 App component 리렌더링
→ App component 리렌더링으로 child component 인 <Login /> 또는 <Home /> 리렌더링
2. re-rendering의 조건 두 가지
- state가 변경되었을 때
- props가 변경되었을 때
만약 parents component의 state 변경의 영향을 받지 않는 grand child component 까지 리렌더링된다면 불필요한 리렌더링이 일어난다. grand child component가 100개, 1000개 이상이라면 매우 비효율적인 코드가 된다.
grand child component 가 props 를 전달받을 때 props 가 변경되지 않은 것 처럼 보일지라도, 메모리에 대한 참조값이 달라졌기 때문에 리렌더링이 발생한다. (리렌더링 될 때 마다 parents 에서 넘겨준 props가 재생성되고, 이전의 props 와 다른 참조값을 가지기 때문)
3.불필요한 re-rendering 해결 방법
parents component가 리렌더링될때 넘겨준 props 의 참조값이 바뀌지 않게 한다면 grand child component는 props 변경의 영향을 받지 않기 때문에 렌더링 최적화에 도움을 줌
- useCallback 사용
- React.memo 사용
1) useCallback (props가 함수일 때)
useCallback은 함수를 memoization 해주는 리액트 훅이다.
즉, 기존에 수행한 연산의 결과 값을 메모리에 저장해두고, 필요할 때 재사용하는 기법이다. 두번째 인수로 전달한 dependency가 변경되지 않는 이상, 컴포넌트가 리렌더링 될 때 마다 변수에 같은 참조값이 할당됨
useCallback을 사용하면 두가지 단계(Render Phase 와 Commit Phase)로 구성된 렌더링에서 한가지 단계만을 수행하면 되기 때문에 최적화에 도움이 된다.
아래는 렌더링의 두가지 단계이다.
렌더링 두 가지 단계
- Render Phase : 컴포넌트(함수) 호출 → 2) React Element 반환 → 3)새로운 Virtual DOM 생성하거나 또는 이미 한번 이상 렌더링이 된 상태라면 재조정(Reconciliation) 과정을 거친 후 Real DOM의 업데이트 필요 여부 체크
- React Element란? 리액트 컴포넌트가 호출되면(렌더링 되면), 리액트는 React.createElement() 메소드를 내부적으로 호출한다. 이 메소드는 아래의 오브젝트를 반환한다.
const App = () => { return <p className="danger">Hello React</p>; }; console.log(App()); // { // $$typeof: Symbol(react.element) // "type": "p", // "key": null, // "ref": null, // "props": { // "children": "Hello React", // "className": "danger" // }, // "_owner": null, // "_store": {} // }
- https://www.robinwieruch.de/react-element-component/
- 재조정(Reconciliation) 과정이란? 이전 Virtual DOM과 현재 VIrtual DOM을 비교하여 업데이트가 필요한지 확인하는 과정이다.
2. Commit Phase : Render Phase 에서 체크해뒀던 DOM에서의 변경이 필요한 부분들을 Real DOM에 반영해주는 단계이다. 만약 변경할 부분이 없다면 Commit Phase 는 skip 된다.
즉, 리액트에서 렌더링이 발생할 때 마다 재조정 과정을 포함한 render phase를 거치고, 필요하다면 그 후 commit phase 를 수행한다.
위의 useCallback을 사용하면, Parents component 가 리렌더링 되어도 props 함수가 같은 값을 참조하기 때문에 DOM update를 하지 않아도 되고, 결국 Render phase 만 실행되고 commit phase는 생략된다. (이외 모든 전제 조건은 같다라는 가정 하에) 따라서 useCallback은 렌더링 최적화의 방법 중 하나가 된다.
2) React.memo
React.memo : 전달받은 props 가, 이전에 가지고 있던 props 와 비교했을때 동일하면 re-rendering을 막아준다. 또한 렌더링 되어있던 결과를 재사용하는 고차 컴포넌트(HOC)이다.
- HOC (Higher Order Component) : HOC는 컴포넌트 로직을 재사용하기 위해 사용되고, 컴포넌트ㅡㄹ 가져와 새 컴포넌트를 반환하는 함수이며 대표적으로 Redux 사용 시의 connect 함수(store 와 연결하기 위한 third-party library 중)가 있다.그러나 함수형 리액트를 주로 사용하게 되면서 react life cycle과 함께 효율적으로 사용할 수 있는 HOC에 대한 관심은 줄어들고 있다. Cross-Cutting Concerns 에 HOC를 주로 사용하는데, 이는 각 계층을 넘어 공통으로 필요한 관심사에 필요한 정보를 주기 위해 만들어진 개념이다.
- 고차 컴포넌트 – React
또한 비교 방법을 이용해 리렌더링 과정을 생략할 수 있다.
React.memo는 기본적으로 얕은 비교를 사용하여 이전의 props 와 현재 전달받은 props 를 비교한다.
1.얕은 비교란?
- 원시타입의 데이터인 경우 값이 같은지 비교
- 참조타입의 데이터인 경우 참조 값이 같은지 비교 (객체) → 객체를 state로 사용하게 될 경우, 값이 같더라도 참조값이 다르기 때문에 리렌더링이 발생함
2. 깊은 비교란?
- 객체(참조 타입)의 경우에도 얕은 비교와는 다르게 값 또한 비교한다.
즉, useCallback으로 컴포넌트가 re-rendering 되어도 props의 참조값이 변하지 않게 하고 (Commit phase 생략) , React.memo를 통해 props를 얕은비교한 뒤 re-rendering을 막는 방법을 통해 Render phase까지 생략해줌으로서 리액트 렌더링을 최적화할 수 있다.
3) useMemo (prop가 함수가 아닌 객체일때)
앞서 언급했던 useCallback은 함수를 memoization 해주는 hook 이다.
만약 props 가 함수가 아닌 객체라면 , parents 컴포넌트가 리렌더링 될 때 마다 객체가 새로 생성되기 때문에 새로운 참조값이 할당되고, React.memo를 제대로 수행할 수 없게 된다.
이때는 값에 대한 memoization을 담당하는 훅인 useMemo를 사용하면 된다.
const memoizedItem = useMemo(() => item, [])
child component 가 넘겨받는 props가 함수인지, 객체인지에 따라 다르게 memoization을 활용하면 불필요한 렌더링을 줄일 수 있다.
참고) memoization이란?
What is Memoization? How and When to Memoize in JavaScript and React
What is Memoization? How and When to Memoize in JavaScript and React
Re-rendering | useCallback | 2 Rendering Phases | React.memo 와 HOC | 얕은 비교와 깊은 비교 | useMemo
'Computer Programming > React' 카테고리의 다른 글
TIL - Redux 기초 (0) | 2023.06.26 |
---|---|
HTML <a>태그의 target 사용 시 주의점 (rel = "noreferrer") (0) | 2023.06.22 |
React 앱에서 Redux 사용예시 (Counter App) (0) | 2023.05.18 |
Redux 시작하기 / 기본 구조 (Reducer, Subscribe, Dispatch란?) (0) | 2023.05.17 |
리액트 React Side effects와 useEffect hook (0) | 2023.05.16 |