useMemo
useMemo는 비용이 큰 연산에 대한 결과를 저장해두고 이 저장된 값을 반환하는 훅이다.
import { useMemo } from 'react'
const memoizedVAlue = useMemo(() => expensiveComputation(a,b),[a,b])
첫번째 인수로는 어떤 값을 반환하는 생성함수를, 두번째 인수로는 해당 함수가 의존하는 값의 배열을 반환한다. useMemo는 의존성 배열의 값이 변경되지 않으면 함수를 재실행하지 않는다.
이런 메모이제이션은 값이 아니라 컴포넌트에 적용하는 것 또한 가능하다!
import React, { useEffect, useMemo, useState } from "react";
function ExpensiveComponent({ value }: { value: number }) {
useEffect(() => {
console.log("reder!!!");
});
return <span>{value + 1000}</span>;
}
const Global = () => {
const [value, setValue] = useState(10);
const [, triggerRendering] = useState(false);
const MemoizedComponent = useMemo(
() => <ExpensiveComponent value={value} />,
[value]
);
function handleChange(e: any) {
setValue(+e.target.value);
}
function handleClick() {
triggerRendering((prev) => !prev);
}
return (
<>
<input type="text" value={value} onChange={handleChange} />
<button onClick={handleClick}>렌더링 발생!</button>
{MemoizedComponent}
</>
);
};
/* STYLE */
export default Global;
위코드를 실행시켜주면 렌더링 발생 버튼을 아무리 눌러도 render가 발생하지 않는다.
즉 useMemo는 어떠한 값을 계산하는 경우 비용이 많이 들때 사용할 수 있다. 그렇다면 어떤 경우가 비용이 많이 드는 연산일까?
저번글을 참조해보자.
useCallback
useMemo가 값을 기억한다면 useCallback은 인수로 넘겨받은 콜백 자체를 기억한다. 즉, 특정함수를 새로 만들지 않고 재사용한다.
import React, { memo, useEffect, useState } from "react";
const ChildComponent = memo(({ name, value, onChange }: any) => {
useEffect(() => {
console.log("render!!!!", name);
});
return (
<>
<h1>
{name} {value ? "켜짐" : "꺼짐"}
</h1>
<button onClick={onChange}> toggle</button>
</>
);
});
const MyComponent = () => {
const [status1, setStatus1] = useState(false);
const [status2, setStatus2] = useState(false);
const toggle1 = () => {
setStatus1(!status1);
};
const toggle2 = () => {
setStatus2(!status2);
};
return (
<div>
<ChildComponent
name="1"
value={status1}
onChange={toggle1}
></ChildComponent>
<ChildComponent
name="2"
value={status2}
onChange={toggle2}
></ChildComponent>
</div>
);
};
/* STYLE */
export default MyComponent;
위 예시는 memo를 사용하여 컴포넌트를 감싸고 있다. 그럼에도 자식컴포넌트들 전체가 렌더링되고 있다.
ChildComponent에 memo를 사용하여 name,value,onChange의 값을 기억하고 이값이 변경되지 않으면 렌더링되지 않는다.
그럼에도 1번 을 눌러도 2번이 같이 바뀌는 이유는 onChange로 넘기는 함수가 재생성되고 있기 때문이다.
이런 경우 useCallback을 사용하여 함수의 메모이제이션을 할 수 있다. 사용방법은 useMemo와 유사하다.
import React, { memo, useCallback, useEffect, useState } from "react";
const ChildComponent = memo(({ name, value, onChange }: any) => {
useEffect(() => {
console.log("render!!!!", name);
});
return (
<>
<h1>
{name} {value ? "켜짐" : "꺼짐"}
</h1>
<button onClick={onChange}> toggle</button>
</>
);
});
const MyComponent = () => {
const [status1, setStatus1] = useState(false);
const [status2, setStatus2] = useState(false);
const toggle1 = useCallback(() => {
setStatus1(!status1);
}, [status1]);
const toggle2 = useCallback(() => {
setStatus2(!status2);
}, [status2]);
return (
<div>
<ChildComponent
name="1"
value={status1}
onChange={toggle1}
></ChildComponent>
<ChildComponent
name="2"
value={status2}
onChange={toggle2}
></ChildComponent>
</div>
);
};
/* STYLE */
export default MyComponent;
useCallback 구현
기본적으로 useCallback은 useMemo를 사용해서 구현할 수 있다. 이는 리액트 공식문서에서도 나올 수 있는 사실이다.
import { useMemo } from "react"
export function useCallback(callback,args){
currentHook = 8
return useMemo(() => callback,args)
}
useMemo와 useCallback의 차이점은 메모이제이션을 하는 대상이 변수냐 함수냐의 차이다. 단 이렇게 useMemo를 활용해서 useCallback을 구현하면 코드가 불필요하게 길어지기 때문에 리액트에서 제공하는 것으로 생각할 수 있다.
useRef
useState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점이 있다. useState와의 큰 차이점 두가지는 아래와 같다.
1. useRef는 반환값인 객체 내부에 있는 current로 값에 접근 & 변경이 가능하다.
2. useRef는 그 값이 변해도 렌더링이 발생하지 않음
useRef로 useState를 흉내내도 렌더링되지 않는다.
import { useRef } from "react";
function RefCompoennt() {
const count = useRef(0);
function handleClick() {
count.current += 1;
}
return <button onClick={handleClick}>{count.current}</button>;
}
위 컴포넌트의 버튼을 아무리 눌러도 변경된 count값이 렌더링 되지 않는다.
렌더링에 영향을 미치지 않는 값을 관리한다는게 이해가 되지 않을 수 있다. 아래 예제를 보자.
let value = 0;
function RefCompoennt() {
function handleClick() {
value += 1;
}
return <button onClick={handleClick}>{value}</button>;
}
위 방식으로 구현해도 분명 렌더링에 영향을 주지 않는 고정된 값을 지정할 수 있다. 단 해당 방식은 몇가지 문제점이 존재한다.
1. 컴포넌트가 렌더링되지 않은 경우에도 value라는 값이 기본적으로 존재하며 메모리에 악영향을 미친다.
2. Component가 여러번 생성되는 경우 컴포넌트가 가리키는 값이 value로 동일하다. 대부분 컴포넌트 인스턴스 하나당 하나의 값이 필요한 경우가 일반적이기 때문에 맞지 않다.
useRef를 사용하면 위 두가지 문제를 해결할 수 있다. 컴포넌트가 렌더링 될때만 생성되고 컴포넌트 인스턴스가 여러개라도 별개의 값을 바라본다.
useRef를 사용하는 가장 일반적인 경우는 DOM에 접근할 필요가 있을때이다.
import React, { useEffect, useRef } from "react";
const MyComponent = () => {
const inputRef = useRef<any>();
console.log(inputRef.current); // 렌더링 전
useEffect(() => {
console.log(inputRef.current);
}, [inputRef]);
return <input ref={inputRef} type="text" />;
};
/* STYLE */
export default MyComponent;
useRef는 최초에 넘겨받은 기본값을 정의하고 있다. 이를 활용하여 useState의 이전 값을 저장하는 usePrevious()와 같은 훅을 구현할 수 있다.
import React, { useEffect, useRef, useState } from "react";
function usePrevious(value: any) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]); // value가 변경되면 그 값을 ref에 넣어둔다.
return ref.current;
}
const MyComponent = () => {
const [counter, setCounter] = useState(0);
const previousCounter = usePrevious(counter);
function handleClick() {
setCounter((prev) => prev + 1);
}
return (
<div>
<button onClick={handleClick}>
{counter},{previousCounter}
</button>
</div>
);
};
/* STYLE */
export default MyComponent;
이게 가능한 이유는 usePrevious가 실행되고 return으로 current값을 반환한 다음, 렌더링이 되고 ref.current값이 바뀌기 때문이다.
useRef구현
실제 리액트와는 조금 다르지만 Preact에서 구현은 아래처럼 되어있다.
export function useRef(initialValue) {
currentHook = 5;
return useMemo(() => ({ current: initialValue }), []);
}
값이 변경되어도 렌더링 되면 안된다는 점, 실제값은 객체 형태로 되어있는 점을 기억하자.
'FrontEnd > Deep Dive' 카테고리의 다른 글
[React] Deep Dive 모던 리액트(11) 사용자 정의 훅 & 고차 컴포넌트 (0) | 2023.12.24 |
---|---|
[React] Deep Dive 모던 리액트(10) useContext,useReducer,기타 훅들 (1) | 2023.12.23 |
[React] Deep Dive 모던 리액트(8) useState & useEffect (1) | 2023.12.21 |
[React] Deep Dive 모던 리액트(7) 렌더링 & 메모이제이션 (1) | 2023.12.20 |
[React] Deep Dive 모던 리액트(6) 클래스형&함수형 컴포넌트 (0) | 2023.12.19 |