React 앱은 컴포넌트를 통해 작업을 수행한다. 컴포넌트는 React에게 해당 컴포넌트의 출력을 JSX 코드로 반환하는 Function 혹은 Class이다.
내부적으로 state
와 props
, context
를 가지며, props
와 context
의 변화는 결국 state
의 변경으로 이어진다.
state
는 컴포넌트 혹은 어플리케이션 전체에 영향을 주는 상태값을 나타내며, 변경된 state
를 지닌 컴포넌트는 재평가되고, 재호출 된다. 이 때 전체 함수/객체를 재실행하고 이를 통해 코드를 전부 리빌드한다.
React는 컴포넌트들을 관리할 뿐, 웹이나 브라우저와는 관계가 없다. 어떻게 컴포넌트를 다루는지는 알고 있지만 이러한 컴포넌트에 실제 HTML요소가 존재하는지에 대해서는 관여하지 않는다.
React는 컴포넌트 내부의 state
에 대한 변경만을 감지한 후 변경된 정보를 React DOM과 같은 인터페이스에 전달한다.
React DOM은 실제 DOM에 대한 작업을 하며 사용자가 직접 접하는 UI에 대한 작업을 담당한다.
React 내부의 state
값이 업데이트 되면 React는 컴포넌트 함수를 다시 호출한다. 업데이트된 state
정보를 토대로 React DOM은 갱신 전후의 상태값의 차이를 인식한 후 React가 컴포넌트 트리를 통해 구성한 가상 스냅샷인 Virtual DOM과 일치하도록 실제 DOM을 조작한다.
즉 컴포넌트가 재호출 된다고 실제 DOM이 업데이트되는 것이 아닌 컴포넌트의 state
와 트리, 그리고 현재 state
사이의 차이점을 기반으로 변경이 필요할 때만 업데이트된다.
실제 DOM을 통한 업데이트는 가상 스냅샷 간의 차이점만 반영된다.(실제 변경이 일어난 DOM만 변경된다.)
DOM의 업데이트 없이 컴포넌트의 호출 및 재평가만 이루어진다면 실제 성능에 미치는 영향은 크지 않을 수 있다.
그러나 부모 컴포넌트의 재평가가 이루어질 경우 모든 자식 컴포넌트들의 재평가 또한 이루어지게 되고(실제 state
값이 바뀌지 않았다 할지라도)
const App = () => {
console.log('App running...');
const [showParagraph, setShowParagraph] = useState(false);
const toggleParagraphHandler = () => {
setShowParagraph(prevState => !prevState);
}
return (
<div className="app">
<h1>Hi there!</h1>
<DemoOutput show={false}/>
<Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
</div>
);
}
export default App;
const DemoOutput = props => {
console.log('DemoOutput running...');
return <MyParagraph>{props.show ? 'This is new!' : ''}</MyParagraph>
}
export default DemoOutput;
const MyParagraph = props => {
console.log('MyParagraph running...');
return <p>{props.children}</p>
}
export default MyParagraph;
위의 예제에서 부모 컴포넌트인 App
의 state
값인 showParagraph
가 바뀔 경우 자식 컴포넌트인 DemoOutput
와 MyParagraph
는 그 값이 변하지 않았다 할지라도 재호출이 일어난다.
실제 DOM은 변경된 값이 없기 때문에 업데이트 되지 않지만, 반복되는 컴포넌트의 재호출은 결과적으로 불필요한 리소스의 낭비가 될 수 있다.(무엇보다 찝찝하다...)
때문에 불필요한 컴포넌트의 호출을 막기 위해 컴포넌트에서 받는 props
의 값에 대한 변경이 있을 경우에만 해당 컴포넌트를 호출하도록 React.memo()
를 사용할 수 있다.
const DemoOutput = props => {
console.log('DemoOutput running...');
return <MyParagraph>{props.show ? 'This is new!' : ''}</MyParagraph>
}
export default React.memo(DemoOutput);
React.memo()
는 입력되는 props
의 모든 신규 값을 확인한 후 기존의 props
값과 비교하여 변경이 존재할 경우에만 컴포넌트를 호출한다. 당연하게도 해당 컴포넌트의 자식 컴포넌트까지 적용되며, 이를 통해 컴포넌트를 최적화할 수 있다.(Class형 React에서는 사용할 수 없다.)
이렇게 최적화가 가능하다면 왜 모든 컴포넌트에 적용하지 않을까? 이는 최적화에도 비용이 발생하기 때문이다.
React.memo()
를 사용할 경우 부모 컴포넌트의 state
값이 변경될 때마다 해당 자식 컴포넌트로 이동해 props
를 비교하는 작업을 거치게되며, 이 때 기존 props
를 저장할 공간과 신규 props
와 기존 값을 대조하는 작업이 필요하게 된다. 이러한 작업들은 각각 개별적인 성능 비용을 발생시킨다.
즉 해당 컴포넌트를 재호출하는데 지불하는 비용과 props
를 비교하는 성능 비용에 대한 trade off가 발생하는 것이다.
해당 컴포넌트에서 받는 props
의 개수와 컴포넌트의 복잡도, 자식 컴포넌트의 개수 등을 고려해야 한다.
결과적으로memo
를 사용하고자 하는 컴포넌트가 컴포넌트 트리에서 상위에 위치해 있고, 자식 컴포넌트의 수가 많을수록 효율적이라고 할 수 있지만, props
의 값에 대한 변경이 자주 일어날 수록 그 효과가 미미하다.
기본적으로 React.memo()
는 props
의 값에 대한 동일성을 비교하기 때문에, 컴포넌트 재호출시 새롭게 생성되는 참조값에 대해서는 적용되지 않으며, 이는 함수도 동일하게 동작한다.
그러나 useCallback()
hook을 통해 props
를 통해 받는 함수도 최적화할 수 있다.
useCallback()
은 기본적으로 컴포넌트 실행 전반에 걸쳐 함수를 저장하는 기능을 제공해주는 hook으로, props
를 통해 받는 함수를 저장하고 컴포넌트 호출시 해당 함수에 대한 재생성을 방지할 수 있다.
function App() {
console.log('App running...');
const [showParagraph, setShowParagraph] = useState(false);
const toggleParagraphHandler = useCallback(() => {
setShowParagraph(prevState => !prevState);
},[]);
return (
<div className="app">
<h1>Hi there!</h1>
<DemoOutput show={false}/>
<Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
</div>
);
}
export default App;
useCallback()
은 첫번째 인자로 저장하고자 하는 함수를 받으며, 두번째 인자로 해당 함수에 대한 의존성 배열을 받는다. 이 때 useState()
의 set함수와 같이 React에서 변경되지 않음을 보장하는 값은 생략 가능하다.
Javascript에서 함수는 클로저이다. 즉 함수가 정의되면, 해당 함수에서 사용되는 함수 외부의 값을 저장한 후 해당 함수 호출시 저장된 시점의 값을 사용하게 된다.
일반적인 경우 이는 함수 호출시 정확한 값에 접근할 수 있게 해주는 매우 유용한 기능이지만, useCallback()
을 사용할 경우 React에서 해당 함수를 저장한 후 state
값이 변하고, 컴포넌트가 다시 호출되어도 재생성하지 않기 때문에 해당 함수는 처음 시점의 변수 값만을 사용하게 된다.
따라서 useCallback()
에서 사용되는 외부 state
값은 의존성 배열에 추가하여 state
값에 대한 변경이 일어날 경우 해당 함수를 재생성하여, 정확한 현재 state
값을 사용할 수 있도록 해야한다.
React에서 state
변경 함수의 호출을 통해 state
의 값이 변경될 경우 즉시 실행되는 것이 아닌, 업데이트에 대한 예약을 걸어둔 후 컴포넌트가 재호출될 때 업데이트 된다.
이러한 작업은 대부분 즉각적이라 할 수 있을 정도로 빠르게 처리되지만, 여러 예약이 동시에 존재할 경우 갱신이 지연될 수 있으며, 결과적으로 변경되지 않은 값을 받게 되는 경우가 발생할 수 있다.
따라서 상태 변경에 대한 최신의 state
값을 받기 위해서는 미완료된 최신의 변경 값을 사용할 수 있도록 함수 형태로 상태 변경 함수를 호출해야 한다.
setSomething(prevState => 'change something');
한개 이상의 state
변경 작업이 어떠한 callback이나 Promise 없이 같은 코드 블럭에 속한다면 React는 하나의 동기화 프로세스에서 같이 실행한다.
useEffect
는 state
또는 종속된 값이 변경될 때마다 의존성 메커니즘을 통해 내부에 선언된 식을 재실행함으로 컴포넌트 또한 재실행되기 때문에 미완료된 상태 변경 작업이 빠짐없이 실행된다.
따라서 이전 상태에 따른 업데이트를 원할 경우 상태 갱신 예약을, 다른 상태값에 따른 업데이트를 원할 경우 useEffect
를 사용하는 편이 좋다.
useMemo
는 데이터를 저장하고, 의존성 배열에 추가된 값이 메모리상에서 변경될 때만 해당 데이터를 재정의 하여 사용한다.
const {items} = props;
const sortedList = useMemo(() => {
return items.sort((a, b) => a - b);
}, [items])
컴포넌트의 재호출시 불필요한 연산이나 데이터 변경을 막아야할 경우 사용할 수 있다.