리액트가 동작하는 방식
- 리액트는 사용자 인터페이스 구축을 위한 자바스크립트 라이브러리이며 컴포넌트를 사용하고 이에 대한 업데이트 역시 컴포넌트를 통해 합니다.
- 리액트는 웹을 모르고 브라우저와는 전혀 관계가 없습니다. 이는 어떻게 컴포넌트를 다루는지는 알고 있지만 이러한 컴포넌트에 HTML 요소들이 포함되어 있는지 아니면 아예 허구적인 요소들이 있는지에 대해서는 상관하지 않습니다.
- 화면에 표시해 주는 것은 리액트 DOM이 고려할 것들이며 컴포넌트의 재평가를 통해 차이점이 있을 경우에만 리렌더링을 하게 됩니다.
- 리액트 내에서 차이점을 가상으로 비교하는 건 메모리 안에서만 발생하기 때문에 간편하고 자원도 적게 듭니다.
- 하지만 실제 DOM을 변경하는 작업은 성능 부하가 많이 발생하기 때문에 최대한 그 차이점을 적게 인지하게 하는 것이 중요합니다.
간단히 버튼으로 이름을 온 오프하는 코드를 구성하고 시작해 봅시다.
import React from 'react';
import Name from './Name';
import './App.css';
function App() {
const [showName, setShowName] = useState(false);
const toggleNameHandler = () => {
setShowName(!showName);
};
return (
<div>
<h1>My Name is...</h1>
<Name show={showName} />
<button onClick={toggleNameHandler}>Toggle</button>
</div>
);
}
export default App;
import React from "react";
const Name = (props) => {
return (
<p>{props.show? 'iltae': ''}</p>
);
}
export default Name;
App 컴포넌트는 Name 자식 컴포넌트를 가지고 있으며 showName을 props로 내려주고 있습니다.
아래와 비교하여 차이점을 봅시다.
function App() {
const [showName, setShowName] = useState(false);
const toggleNameHandler = () => {
setShowName(!showName);
};
return (
<div>
<h1>My Name is...</h1>
<Name show={false} /> <!--여기-->
<button onClick={toggleNameHandler}>Toggle</button>
</div>
);
}
위와 비교하였을 때 props로 내려주는 값이 버튼에 의해 변화되는 상태가 아닌 고정적인 값입니다.
그럼에도 Name 컴포넌트는 App 컴포넌트의 일부이기 때문에 항상 재실행됩니다.
이렇게 연결된 모든 컴포넌트를 항상 재실행하지 않고 전해지는 props가 변경될 때만 자식 컴포넌트를 재실행하게 할 순 없을까요?
React.memo()
import React from "react";
const Name = (props) => {
return (
<p>{props.show? 'iltae': ''}</p>
);
}
export default React.memo(Name); // 여기
이렇게 Name 컴포넌트를 export 할 때 React.memo로 감싸면 물려받은 props가 변할 때에만 컴포넌트를 재실행하여 불필요한 리렌더링을 막을 수 있습니다.
그럼 여기서 드는 의문은 왜 모든 컴포넌트에 이런 최적화를 이루지 않느냐입니다.
이 이유는 최적화 역시 비용이 들기 때문입니다.
기존의 props 값을 비교하기 위한 저장 공간이 필요하게 되고 비교하는 작업도 수행해야 합니다.
따라서, 이 성능 효율은 어떤 컴포넌트를 최적화하느냐에 따라 달라집니다.
useCallback()
위의 개념을 이어나가 버튼을 자식 컴포넌트로 만들어보고 시작하겠습니다.
import React from 'react';
import classes from './Button.module.css';
const Button = (props) => {
return (
<button
type={props.type || 'button'}
className={`${classes.button} ${props.className}`}
onClick={props.onClick}
disabled={props.disabled}
>
{props.children}
</button>
);
};
export default React.memo(Button); // 여기
<Button onClick={toggleParagraphHandler}>Toggle</Button>
이렇게 Button 컴포넌트에도 메모를 씌었는데도 위의 Name 컴포넌트와는 다르게 계속 재실행됩니다.
그 이유는 내려받는 props가 함수이고 곧 함수는 객체이기 때문입니다.
이 함수는 재실행될 때 같은 기능을 하는 함수를 새로 만드는 것이지 기존에 것을 이용하는 것이 아닙니다.
예시와 같이 그저 같은 기능을 가진 다른 함수 객체이기 때문에 props에서 이전 값과 비교할 때 다른 값이라 판단하는 것입니다.
그럼 함수는 어떻게 재생성을 방지할까?라는 생각에서 시작합니다.
useCallback 훅은 컴포넌트 실행 전반에 걸쳐 함수를 저장할 수 있게 하는 훅입니다.
리액트에 함수를 저장해 매번 실행할 때마다 이 함수를 재생성할 필요가 없다는 것을 알립니다.
이렇게 되면 동일한 함수 객체가 메모리의 동일한 위치에 저장되므로 이를 통해 비교 작업을 할 수 있습니다.
이렇게 두 객체가 같은 메모리 안의 같은 위치를 가리키게 하는 것이 useCallback의 역할입니다.
사용법은 간단하게 함수를 첫 번째 인자로 감싸면 됩니다.
const toggleNameHandler = useCallback(() => {
setShowName(!showName);
},[]);
어떤 함수가 절대 변경되어서는 안 된다면 이 훅을 사용해 함수를 저장하면 됩니다!
훅의 두 번째 인자 의존성 배열에 대하여...
이 배열의 이용은 함수를 저장하게 되면서 나타나는 문제와 연결됩니다.
이건 자바스크립트 클로저 개념과도 관련이 있습니다. 클로저에 관한 내용은 아래 링크에 있습니다.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
Closures - JavaScript | MDN
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closur
developer.mozilla.org
const [showName, setShowName] = useState(false);
const [allowToggle, setAllowToggle] = useState(false);
const toggleNameHandler = useCallback(() => {
if (allowToggle) {
setShowName(!showName);
}
},[]);
const allowToggleHandler = () => {
setAllowToggle(true);
}
예시로 버튼을 활성화하는 버튼을 하나 더 만들어 그 버튼을 눌러야 이름을 온 오프하는 버튼이 활성화된다는 개념으로 이렇게 구성을 해봅니다.
여기서 문제는 toggleNameHanlder는 처음 앱이 실행될 때 저장되기 때문에 allowToggle은 false로 고정되어 저장됩니다.
그래서 이 예시에선 절대 showName이 true가 되지 않고 있습니다...
const toggleNameHandler = useCallback(() => {
if (allowToggle) {
setShowName(!showName);
}
},[allowToggle]);
이렇게 두 번째 인자에 변하는 값을 넣어주고 새로운 값이 들어오면 함수를 재생성하고 이 새로운 함수를 저장할 수 있게 합니다.
useMemo()
useMemo는 useCallback이 함수에 대한 것을 저장하듯 모든 종류의 데이터를 저장할 수 있습니다.
const listItems = useMemo(() => [5, 3, 1, 10, 9], [])
이렇게 특정 배열을 메모리에 저장할 수도 있고
const sortedList = useMemo(() => {
return items.sort((a, b) => a - b);
}, [items]);
데이터 재계산 같은 성능 집약적인 작업도 저장해 재실행을 방지해 최적화에 더 도달할 수 있습니다.
하지만 함수를 기억하는 것이 훨씬 더 도움이 되고 데이터를 기억하는 일보다 빈도수가 더 많기 때문에 useCallback보단 이용이 적습니다.
'REACT' 카테고리의 다른 글
useContext 훅 (5) | 2023.03.03 |
---|---|
useReducer 훅 (5) | 2023.03.02 |
커스텀 훅으로 HTTP 요청 리팩토링 (8) | 2023.02.17 |
커스텀 훅(Custom Hook) (10) | 2023.02.16 |
HTTP 요청 보내기 (8) | 2023.02.13 |