React는 컴포넌트를 렌더링 한 뒤, 이전 렌더된 결과와 비교하여 Dom 업데이트를 결정한다. 이 때 재렌더가 필요없는 컴포넌트의 렌더를 방지하여 이 업데이트 속도를 높일 수 있는 방법들이 있는데 정확한 역할과 차이점을 구분하기 위해 정리해 보고자 한다.
React.memo는 Higher-Order Components(HOC)이다.
(HOC란 컴포넌트를 인자로 받아서 새로운 컴포넌트를 return해주는 구조의 함수)
// Auth라는 HOC를 통해 권한에 따라 라우팅 처리
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<NavBar />
<div>
<Switch>
<Route exact path="/" component={Auth(LandingPage, null)} />
<Route exact path="/login" component={Auth(LoginPage, false)} />
<Route exact path="/register" component={Auth(RegisterPage, false)} />
<Route
exact
path="/video/upload"
component={Auth(VideoUploadPage, true)}
/>
<Route
exact
path="/video/:videoId"
component={Auth(VideoDetailPage, null)}
/>
<Route
exact
path="/subscription"
component={Auth(SubscriptionPage, null)}
/>
</Switch>
</div>
<Footer />
</Suspense>
);
}
export default App;
// Auth validation example
import React, { useEffect } from 'react';
import { auth } from '../_actions/user_actions';
import { useSelector, useDispatch } from "react-redux";
export default function (SpecificComponent, option, adminRoute = null) {
function AuthenticationCheck(props) {
let user = useSelector(state => state.user);
const dispatch = useDispatch();
useEffect(() => {
//To know my current status, send Auth request
dispatch(auth()).then(response => {
//Not Loggined in Status
if (!response.payload.isAuth) {
if (option) {
props.history.push('/login')
}
//Loggined in Status
} else {
//supposed to be Admin page, but not admin person wants to go inside
if (adminRoute && !response.payload.isAdmin) {
props.history.push('/')
}
//Logged in Status, but Try to go into log in page
else {
if (option === false) {
props.history.push('/')
}
}
}
})
}, [])
return (
<SpecificComponent {...props} user={user} />
)
}
return AuthenticationCheck
}
일반적인 Component는 props를 넘겨받아 UI에 활용하는 반면, HOC는 리액트의 API가 아니라 리액트가 컴포넌트를 구성하는데 있어서의 일종의 패턴이라고 보면된다.
기본적으로 리액트는 Shallow copy를 실행한다.(참조값만 비교)
즉, state가 변경되거나, 새로운 컴포넌트가 렌더링 되는 시점에서, shallow copy를 통해 같은 값인지 판단하고 렌더링 여부를 결정하게 된다.
그렇기 때문에 primitive type이 아닌 객체,배열,함수와 같은 reference type은 같은 참조가 아니라면 새로운 값으로 판단하게 된다.
예컨데 같은 값의 props라도 컴포넌트의 state가 변경되면 shallow copy에 의해 새로운 값으로 인식하는 것이다.
하나의 컴포넌트가 똑같은 props를 넘겨 받았을 때 같은 결과를 렌더링 하고 있다면 React.memo를 사용하여 불필요한 컴포넌트 렌더링을 방지 할 수 있다.
React.memo를 사용할 경우 이전과 같은 props가 들어올때는 렌더링 과정을 스킵하고 가장 최근에 렌더링된 결과를 재사용 한다.
React.memo는 넘겨받은 props의 변경 여부만을 체크한다. 하지만 컴포넌트 내부에서 useState같은 훅을 사용 하고 있는 경우에는 상태가 변경 되면 리렌더링 된다.
props 자체는 primivite type이기 때문에 값만 같아도 되지만, 함수나 객체는 그렇지 않다.
그러므로 useCallback, useMemo를 통해 특정 props에 dependency를 걸어줌으로써 렌더링 횟수를 줄일 수 있다.
그러나!
useCallback만으로는 하위컴포넌트의 리렌더링을 막을 수 없다.
부모 컴포넌트에서 정의한 useCallback은 자식 컴포넌트에서 사용할 때 아무 효력도 없기 때문이다.
자식 컴포넌트에 useCallback에 대한 로직처리가 없다면, 다시 부모의 렌더링 여부에 따라 렌더링 될 뿐이다.
즉, 자식 컴포넌트가 '참조동일성에 의존적인 pureComponent'여야 의미가 있다.
export default React.memo(component);
기본적으로 export 시켜줄 때 컴포넌트명을 React.memo
로 감싸준다.
shouldComponentUpdate를 내장하고 있어 shallow copy를 실행하여 리렌더링을 방지한다.
방법적으론 예시코드 처럼 모듈화 시키는 컴포넌트를 export할때 React.memo로 감싸주면 된다.
이 방식은 '같은 props로 렌더링이 자주일어나는 컴포넌트','렌더링에 리소스 소모가 큰 컴포넌트'에 사용된다.
그러나 무분별하게 사용되는 것은 좋지 않다. 어떠한 경우에는 그저 불필요한 비교 연산만 추가되는 꼴이 되기 때문이다.
위와 같은 방법 외에도 아래 예시처럼 사용할 수도 있다.
// example
const MyComponent = React.memo((props) => {
return (/*컴포넌트 렌더링 코드*/)}
);
넘겨받은 props의 변경 여부는 shallow compare로 비교 되므로, object의 경우 같은 값을 참조 하고 있는지를 비교한다.
(불변성을 고려해야함)
이 비교방식을 커스텀 하고 싶은 경우 아래와 같이 React.memo의 두번째 인자로 넣어주면 된다.
function MyComponent(props) {
/* 컴포넌트 로직 */
}
function areEqual(prevProps, nextProps) {
/*
전달되는 nextProps가 prevProps와 같다면 true를 반환, 같지 않다면 false를 반환해 준다.
*/
}
export default React.memo(MyComponent, areEqual);
useMemo는 메모이즈된 값을 return하는 hook이다.
useMemo는 이전 값을 기억해두었다가 조건에 따라 재활용하여 성능을 최적화 하는 용도로 사용된다. (특정 value를 재사용)
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
인자로 함수와 Dependencies를 넘겨 받는다. 이 때, 2번째 인자로 넘겨준 의존 인자 중에 하나라도 값이 변경되면 1번째 인자의 함수를 재실행한다. 이를 통해 매 렌더링 할때마다 소요되는 불필요한 계산을 피할 수 있다. 만약 Dependencies 인자를 전달하지 않는다면 매번 새롭게 계산하여 return 한다.
특정 상황에서만 동작되어야 하는 함수가, Component의 렌더링 조건에 따라 지속적으로 함수가 실행되는 경우에 사용할 수 있다.
아래 예시를 보자.
const count = countActiveUsers(users);
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onRemove={onRemove} onToggle={onToggle} />
<div>숫자 : {count}</div>
</>
);
이런 경우 countActiveUser는 users가 변화가 있을때만 다시 실행되어야 하는데, 예시코드처럼 로직을 짜면 모든 state변화마다 count가 다시 호출되어 실행된다.
//const count = countActiveUsers(users);
const count = useMemo(() => countActiveUsers(users), [user]);
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onRemove={onRemove} onToggle={onToggle} />
<div>숫자 : {count}</div>
</>
);
이러한 경우에 useMemo를 사용한다. useEffect와 사용법은 유사하다. useMemo의 동작조건을 설정하고, 그 변수에 내가 원하는 상황을 등록해주면 된다.
useRef와의 차이
useMemo는 deps가 변경되기 전까지 값을 기억하고, 실행후 값을 보관하는 역할로도 사용한다. 얘는 복잡한 함수의 return 값을 기억한다는 점에서 useRef와는 다르다. useRef는 특정 값을 기억하는 경우, useMemo는 복잡한 함수의 return값을 기억하는 경우에 사용한다.
useCallback은 리액트의 렌더링 성능을 위해서 제공되는 Hook이다.
컴포넌트가 렌더링 될 때마다 내부적으로 사용된 함수가 새롭게 생성되는 경우,
자식 컴포넌트에 Prop으로 새로 생성된 함수가 넘겨지게 되면 불필요한 리렌더링이 일어날 수 있다. (특정 함수를 재사용)
import React, {useSatate} from 'react';
import {saveToServer} from './api';
import UserEdit from './UserEdit';
function Profile(){
const [name, setName] = useState('');
const [age, setAge] = useState(0);
return (
<div>
<p>{`name is ${name}`}</p>
<p>{`age is ${age}`}</p>
<UserEdit
onSave={() => saveToServer(name, age)}
setName={setName}
setAge={setAge}
/>
</div>
);
}
Profile 컴포넌트가 렌더링 될 때마다 UserEdit 컴포넌트의 onSave 속성값으로 새로운 함수가 전달된다.
UserEdit 컴포넌트에서 React.memo를 사용해도 전달된 Prop이 항상 바뀌므로 불필요한 렌더링이 발생한다.
onSave의 속성값은 name이나 age값이 변경되지 않으면 항상 같아야 한다.
이 같은 문제를 다음과 같이 useCallback 을 사용하여 방지 할 수 있다.
function Profile(){
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const onSave = useCallback(() => saveToServer(name, age), [name, age]);
return (
<div>
<p>{`name is ${name}`}</p>
<p>{`age is ${age}`}</p>
<UserEdit onSave={onSave} setName={setName} setAge={setAge} />
</div>
);
}
useMemo와 마찬가지로 1번째 인자로 함수, 2번째 인자로 Dependencies를 전달한다.
전달된 의존성 인자가 바뀌지 않으면 이전에 생성한 함수가 재사용 된다.
즉, name과 age값이 변경되지 않으면,
UserEdit 컴포넌트의 onSave 속성값으로 항상 같은 함수가 전달되어 리렌더링을 방지 할 수 있다.
useMemo와 유사하게 각 함수마다 useCallback으로 정의하고 조건을 달아주면 된다. 당연히 해당 조건들은 함수내에서 사용하는 모든 변수여야 한다.
자식컴포넌트에 함수를 props으로 줄때는 반드시 useCallback을 사용하여 리렌더링이 안되도록 하자.
잘 읽고 가용~