// Counter 클래스를 선언합니다.
class Counter extends Component {
// props를 받아와서 상위 클래스의 생성자를 호출합니다.
constructor(props) {
super(props);
// state를 초기화합니다.
this.state = {
counter: 0,
};
// handleIncrease 함수에서 this를 사용하기 위해 bind합니다.
this.handleIncrease = this.handleIncrease.bind(this);
}
// handleIncrease 함수를 선언합니다.
handleIncrease = () => {
// state를 업데이트합니다.
this.setState({
counter: this.state.counter + 1,
});
};
// 렌더링 함수를 선언합니다.
render() {
return (
<div>
<p>You clicked {this.state.counter} times</p>
<button onClick={this.handleIncrease}>Click me</button>
</div>
);
}
}
이런 클래스 컴포넌트는 복잡해질수록 이해하기 어려워졌고, 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다는 단점이 있었습니다.
React의 클래스 컴포넌트를 사용하기 위해서는 JavaScript의 this 키워드가 어떤 방식으로 동작하는지 알아야 하는데, 이는 문법을 정확히 알지 못하면 동작 방식 자체를 정확히 이해하기 어렵게 만들곤 했습니다.
React는 점진적으로 클래스 컴포넌트에서 함수 컴포넌트로 넘어갔습니다. 다만 이전까지의 함수 컴포넌트는 클래스 컴포넌트와는 다르게 상태 값을 사용하거나 최적화할 수 있는 기능들이 조금 미진했는데, 그 부분들을 보완하기 위해 Hook이라는 개념을 도입하였습니다.
// useState 훅을 사용하여 counter 변수와 setCounter 함수를 선언한다.
function Counter() {
const [counter, setCounter] = useState(0);
// handleIncrease 함수를 선언한다.
const handleIncrease = () => {
// setCounter 함수를 사용하여 counter 값을 1 증가시킨다.
setCounter(counter + 1);
};
// JSX를 반환한다.
return (
<div>
<p>You clicked {counter} times</p>
<button onClick={handleIncrease}>Click me</button>
</div>
);
}
함수형 컴포넌트는 클래스형 컴포넌트에 비해 훨씬 더 직관적이고, 보기 쉽다는 특징이 있습니다. 이 Counter 컴포넌트에서 숫자를 올리기 위해 상태값을 저장하고 사용할 수 있게 해주는 useState() 가 있는데, 여러분도 익히 알고 있는 이 메서드가 바로 Hook입니다.
다시 말하자면, Counter 컴포넌트에서 useState() Hook을 호출해 함수 컴포넌트(function component) 안에 state를 추가한 형태입니다. 이 state는 컴포넌트가 리렌더링 되어도 그대로 유지될 것입니다. 또한 해당 컴포넌트에서 State Hook은 하나만 사용했지만 때에 따라서 여러 개 사용할 수 있습니다.
Hook은 React 16.8에 새로 추가된 기능입니다. Hook은 class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해줍니다.
Hook은 다르게 말하면 함수형 컴포넌트에서 상태 값 및 다른 여러 기능을 사용하기 편리하게 해주는 메소드를 의미합니다. Hook은 class가 아닌 function으로만 React를 사용할 수 있게 해주는 것이기 때문에 클래스형 컴포넌트에서는 동작하지 않습니다.
render(){
/* 클래스 컴포넌트는 render() 안에서 변수를 작성할 수 있습니다. */
const [counter, setCounter] = useState(0);
...
}
컴포넌트는 기본적으로 상태가 변경되거나 부모 컴포넌트가 렌더링이 될 때마다 리렌더링을 하는 구조로 이루어져 있습니다. 그러나 너무 잦은 리렌더링은 앱에 좋지 않은 성능을 끼칩니다.
useMemo은 특정 값(value)를 재사용하고자 할 때 사용하는 Hook입니다.
function Calculator({value}){
const result = calculate(value);
return <>
<div>
{result}
</div>
</>;
}
해당 컴포넌트는 props로 넘어온 value값을 calculate라는 함수에 인자로 넘겨서 result 값을 구한 후,
만약 여기서 calculate가 내부적으로 복잡한 연산을 해야 하는 함수라 계산된 값을 반환하는 데에 시간이 몇 초 이상 걸린다고 가정해 봅시다. 그렇다면 해당 컴포넌트는 렌더링을 할 때마다 이 함수를 계속해서 호출할 것이고, 그 때마다 시간이 몇 초 이상 소요가 될 것입니다. 이 몇 초의 지연은 렌더링에도 영향을 미칠 것이고, 사용자는 “앱의 로딩 속도가 느리네?”라는 생각을 하게 될 것입니다.
/* useMemo를 사용하기 전에는 꼭 import해서 불러와야 합니다. */
import { useMemo } from "react";
function Calculator({value}){
const result = useMemo(() => calculate(value), [value]);
return <>
<div>
{result}
</div>
</>;
}
여기 value 를 인자로 받는 Calculator 컴포넌트가 있습니다.
value 는 일종의 값으로서, 이 값이 계속 바뀌는 경우라면 어쩔 수 없겠지만, 렌더링을 할 때마다 이 value값이 계속 바뀌는 게 아니라고 생각해 봅시다. 그럼 이 값을 어딘가에 저장을 해뒀다가 다시 꺼내서 쓸 수만 있다면 굳이 calculate 함수를 호출할 필요도 없을 것입니다. 여기서 useMemo Hook을 사용할 수 있습니다.
이런 식으로 useMemo를 호출하여 calculate를 감싸주면, 이전에 구축된 렌더링과 새로이 구축되는 렌더링을 비교해 value값이 동일할 경우에는 이전 렌더링의 value값을 그대로 재활용할 수 있게 됩니다. 이는 메모이제이션(Memoization) 개념과 긴밀한 관계가 있습니다.
메모이제이션(Memoization)은 알고리즘에서 자주 나오는 개념입니다. 기존에 수행한 연산의 결과값을 메모리에 저장을 해두고, 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말합니다. 이 메모이제이션을 적절히 사용한다면 굳이 중복 연산을 할 필요가 없기 때문에 앱의 성능을 최적화할 수 있습니다.
useMemo는 바로 이 개념을 이용하여 복잡한 연산의 중복을 피하고 React 앱의 성능을 최적화시킵니다. 직접 메모이제이션 개념을 이용하여 로직을 구현할 수도 있겠으나, useMemo Hook을 호출한다면 이런 로직을 직접 구현하는 것을 대신해주기 때문에 훨씬 간편하다고 할 수 있습니다.
React Hook은 렌더링 최적화를 위한 Hook도 존재하는데, useCallback과 useMemo가 바로 그 역할을 하는 Hook라고 배웠습니다.
useCallback이란?
useCallback 또한 useMemo와 마찬가지로 메모이제이션 기법을 이용한 Hook입니다. useMemo는 값의 재사용을 위해 사용하는 Hook이라면, useCallback은 함수의 재사용을 위해 사용하는 Hook입니다.
function Calculator({x, y}){
const add = () => x + y;
return <>
<div>
{add()}
</div>
</>;
}
useMemo와 마찬가지로, 해당 컴포넌트가 리렌더링 되더라도 그 함수가 의존하고 있는 값인 x와 y가 바뀌지 않는다고 생각해 봅시다. 그렇다면 함수 또한 메모리 어딘가에 저장해 뒀다가 다시 꺼내서 쓸 수 있을 것입니다.
이때 useCallback Hook을 사용하면 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환합니다. 즉 x와 y값이 동일하다면 다음 렌더링 때 이 함수를 다시 사용합니다.
/* useCallback를 사용하기 전에는 꼭 import해서 불러와야 합니다. */
import React, { useCallback } from "react";
function Calculator({x, y}){
const add = useCallback(() => x + y, [x, y]);
return <>
<div>
{add()}
</div>
</>;
}
function doubleFactory(){
return (a) => 2 * a;
}
const double1 = doubleFactory();
const double2 = doubleFactory();
double1(8); // 16
double2(8); // 16
double1 === double2; // false
double1 === double1; // true
double1과 double2는 같은 함수를 할당했음에도 메모리 주소 값이 다르기 때문에 같다고 보지 않습니다.
JavaScript에서 함수는 객체입니다. 따라서 두개의 함수는 동일한 코드를 공유하더라도 메모리 주소가 다르기 때문에, 메모리 주소에 의한 참조 비교 시 다른 함수로 봅니다.
이는 React 또한 같습니다. React는 리렌더링 시 함수를 새로이 만들어서 호출을 합니다. 새로이 만들어 호출된 함수는 기존의 함수와 같은 함수가 아닙니다.
그러나 useCallback을 이용해 함수 자체를 저장해서 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용한다는 것과 같다고 볼 수 있습니다.
따라서 React 컴포넌트 함수 내에서 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있습니다.
useCallback과 useMemo는 거의 비슷한 것입니다.
하지만 다른 것은 useCallback 은 메모이제이션된 함수를 반환하며.
useMemo 은 메모이제이션된 값를 반환합니다.