React Docs (beta) 번역 및 정리
Built-in React APIs
1. createContext
2. forwardRef
3. lazy
4. memo
📍memo
컴포넌트의 props가 변경되지 않는 경우, 리렌더링을 스킵하게 할 수 있다
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
📍memo(Component, arePropsEqual?)
- memo api로 memoization할 컴포넌트를 래핑한다
- memoized된 버전의 컴포넌트는 리렌더링되지 않는다 (부모 컴포넌트가 리렌더링되어도)
단, props가 바뀌면 리렌더링됨
- memoization 완벽하게 보장되지는 않으며 성능 최적화 용도로만 사용해야 한다
import { memo } from 'react';
const SomeComponent = memo(function SomeComponent(props) {
// ...
});
✅매개변수
컴포넌트
- memoization할 컴포넌트
- forwardRef 처럼 모든 유효한 컴포넌트가 매개변수로 가능
(옵셔널) arePropsEqual
- 2개의 인수를 갖는 함수
1. old props
2. new props
- old props와 new props가 같으면 true 반환
✅반환값
- memoized component 반환
📍예제) props가 바뀌지 않으면 리렌더링 생략
- 일반적으로 리액트에서는 부모 컴포넌트가 리렌더링되면, 그 자식 컴포넌트들도 리렌더링된다
- memo api를 사용하여 props가 바뀌지 않으면 부모 컴포넌트가 리렌더링되어도 리렌더링되지 않는 컴포넌트를 만들 수 있다
- 이 때, 컴포넌트가 memoized 된다고 한다
아래의 Greeting 컴포넌트는 name props가 바뀌지 않으면 부모 컴포넌트가 리렌더링되어도 리렌더링되지 않는다
const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});
export default Greeting;
⭐리액트의 컴포넌트들은 항상 순수 렌더링 로직(순수 함수 처럼)을 가져야 한다
- 즉, props, state, context가 바뀌지 않으면 항상 같은 아웃풋를 가져야한다
이는 memoized 컴포넌트도 마찬가지이다, props, 자체 state, 사용중인 context가 변경되면 리렌더링된다
- App.js
import { memo, useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
Name{': '}
<input value={name} onChange={e => setName(e.target.value)} />
</label>
<label>
Address{': '}
<input value={address} onChange={e => setAddress(e.target.value)} />
</label>
<Greeting name={name} />
</>
);
}
const Greeting = memo(function Greeting({ name }) {
console.log("Greeting was rendered at", new Date().toLocaleTimeString());
return <h3>Hello{name && ', '}{name}!</h3>;
});
name을 props로 갖는 Greeting memoized 컴포넌트는 name이 바뀌면 리렌더링된다
📍모든 곳에 memo를 추가해야 할까?
- memo를 과용하면 가독성을 해칠 수 있다
- 또한 props 값이 조금이라도 바뀌면 리렌더링되므로, 생각보다 memoization 혜택을 볼 수 있는 경우가 제한적이다
⭐memoization을 아낄 수 있는 몇 가지 원칙
1. When a component visually wraps other components, let it accept JSX as children. This way, when the wrapper component updates its own state, React knows that its children don’t need to re-render.
2. 가급적 전역 대신 지역 상태를 사용하고, 일시적인(변화가 일회성) 상태가 컴포넌트 트리 또는 전역 상태 라이브러리에 포함되어 있지 않게 할 것.
3. 순수 리렌더링 로직을 지킬 것
- props, state, context가 동일한데 리렌더링에 따라 다른 결과를 가져온다면, 이는 버그이다
4. 상태를 업데이트하는 불필요한 effect를 제거하라
- React 앱에서의 대부분의 성능 문제는 컴포넌트가 계속 렌더링되게 하는 effect (상태를 변화) 때문에 발생한
5. effect 에서 불필요한 의존성(의존성 배열) 을 제거하라
- 메모이제이션 대신 객체나 함수를 컴포넌트 외부로 빼는 것이 더 효과적인 경우가 많다
리액트 데브 툴즈의 Profiler 기능을 사용하면 어떤 컴포넌트를 메모이제이션했을 때 가장 효과적일지 확인할 수 있다
📍예제) 자체 state가 변경되면 리렌더링되는 memoized 컴포넌트
- props 이외에도 자체 state가 변경되어도 리렌더링된다
import { memo, useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
Name{': '}
<input value={name} onChange={e => setName(e.target.value)} />
</label>
<label>
Address{': '}
<input value={address} onChange={e => setAddress(e.target.value)} />
</label>
<Greeting name={name} />
</>
);
}
const Greeting = memo(function Greeting({ name }) {
console.log('Greeting was rendered at', new Date().toLocaleTimeString());
// memoized component가 갖는 자체 state
// greeting 값이 변경되면 memoization을 무시하고 리렌더링된다
const [greeting, setGreeting] = useState('Hello');
return (
<>
<h3>{greeting}{name && ', '}{name}!</h3>
<GreetingSelector value={greeting} onChange={setGreeting} />
</>
);
});
function GreetingSelector({ value, onChange }) {
return (
<>
<label>
<input
type="radio"
checked={value === 'Hello'}
onChange={e => onChange('Hello')}
/>
Regular greeting
</label>
<label>
<input
type="radio"
checked={value === 'Hello and welcome'}
onChange={e => onChange('Hello and welcome')}
/>
Enthusiastic greeting
</label>
</>
);
}
📍예제) context를 사용하는 memoized compoenet 업데이트하기
- memoization 되더라도, 사용중인 context가 변경되면(subscription 하고 있는 value가 변경되면 리렌더링된다
import { createContext, memo, useContext, useState } from 'react';
const ThemeContext = createContext(null);
export default function MyApp() {
const [theme, setTheme] = useState('dark');
function handleClick() {
setTheme(theme === 'dark' ? 'light' : 'dark');
}
return (
<ThemeContext.Provider value={theme}>
<button onClick={handleClick}>
Switch theme
</button>
<Greeting name="Taylor" />
</ThemeContext.Provider>
);
}
const Greeting = memo(function Greeting({ name }) {
console.log("Greeting was rendered at", new Date().toLocaleTimeString());
// subscription 중인 아래 theme 값이 변경되면 리렌더링 발생
const theme = useContext(ThemeContext);
return (
<h3 className={theme}>Hello, {name}!</h3>
);
});
📍props 변경을 최소화하기
- React에서는 기본적으로 두 컴포넌트의 props가 다른지 비교할 때 shallow compare(Object.is 메서드로 비교)를 사용한다
- memo를 최대한 활용하려면, props가 바뀌는 시간을 최소화해야 한다
- 예를 들어, props가 객체인 경우, useMemo를 사용하여 부모 컴포넌트가 매번 해당 객체를 다시 만들지 않도록 한다
function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
// useMemo를 사용하여 person 객체를 생성
const person = useMemo(
() => ({ name, age }),
[name, age]
);
return <Profile person={person} />;
}
// Profile 컴포넌트는 person 객체가 바뀌지 않으면 (즉, name와 age state가 바뀌지 않으면)
// 새로 만들어지지 않는다
const Profile = memo(function Profile({ person }) {
// ...
});
- props 변경을 최소화하는 더 좋은 방법은 컴포넌트가 해당 props에서 필요한 최소한의 정보만 받게하는 것이다
- 아래처럼 줄이면 된다
function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return <Profile name={name} age={age} />;
}
const Profile = memo(function Profile({ name, age }) {
// ...
});