React Lifecycle을 보기 좋게 정리한 사진이다.
관련 공부가 더 필요하다고 느껴 관련 내용과 훅들을 정리하겠다.

useRef는 저장공간 또는 DOM요소에 접근하기 위해 사용되는 React Hook이다.
Ref는 reference, 즉 참조를 뜻한다.
React를 사용하는 프로젝트에서도 가끔씩 DOM 을 직접 선택해야 하는 상황이 필요하다.
그 때 useRef라는 React Hook을 사용한다.
//생성
const 변수명 = useRef(초기값)
// {current: 초기값} 을 지닌 객체가 반환됨
// 예시1 (DOM 조작 FOCUS)
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []);
<div>
<input ref={inputRef} type="text" placeholder="username" />
<button onClick={login}>로그인</button>
</div>
// 예시 2
const unsavedChangesRef = React.useRef({
unsavedRows: {},
rowsBeforeChange: {},
});
const unsavedRow = unsavedChangesRef.current.unsavedRows[id];
// 예시 3 (useState, useRef, 변수의 차이점)
function App() {
const [stateCount, setStateCount] = useState(0);
const refCount = useRef(0);
let varCount = 0;
function upState() {
setStateCount(stateCount + 1);
console.log("stateCount : ", stateCount);
}
function upRef() {
++refCount.current;
console.log("refCount : ", refCount.current);
}
function upVar() {
++varCount;
console.log("varCount : ", varCount);
}
return (
<div>
<div>stateCount : {stateCount} </div>
<div>refCount : {refCount.current} </div>
<div>varCount : {varCount} </div>
<br />
<button onClick={upState}>state up</button>
<button onClick={upRef}>ref up</button>
<button onClick={upVar}>var up</button>
</div>
);
}
ref up 창을 눌러도 콘솔창에만 출력되고 화면에 렌더링이 되지 않는다.
state up 버튼을 누를때에 화면이 재랜더링 되므로 ref도 갱신되서 화면에 랜더링된다.
var up 버튼으로 변수값을 늘려놔도 state up 버튼을 눌러서 재랜더링을 할 때,
컴포넌트 내부에 있는 let varCount = 0이 계속 실행되므로 값은 계속 초기화된다.
내 컴포넌트가 몇번 렌더링됐는지 알고싶은 상황에서 useRef() 를 사용하는 예시
function App() {
const [count, setCount] = useState(1);
const renderingCount = useRef(1);
useEffect(() => {
console.log("renderingCount : ", renderingCount.current);
++renderingCount.current;
});
/* 잘못된 예시 ( 무한루프 )
// useEffect 안에있는 setRenderingCount()가
// 계속해서 컴포넌트를 리렌더링해서 무한 루프
const [renderingCount, setRedneringCount] = useState(1);
useEffect(() => {
console.log("rendering Count : ", renderingCount);
setRedneringCount(renderingCount + 1);
});
*/
return (
<div>
<div>Count : {count}</div>
<button onClick={() => setCount(count + 1)}> count up </button>
</div>
);
}
변화는 감지하지만 렌더링 발생 X -> 성능향상 (state는 변화시, 재렌더링됨)
DOM요소에 손쉽게 접근 (ref속성 사용)
전생애주기를 통해 유지 (언마운트 되기전까지는 값을 계속 기억함)
useMemo는 컴포넌트의 성능을 최적화 하는데 사용되는 훅이다.
동일한 값을 반환하는 함수를 반복적으로 호출해야하는 상황일 때, 처음 값을 계산할 때 해당 값을 메모리에 저장해 필요할 때마다 다시 계산하지 않고 메모리에서 꺼내서 재사용한다.
리액트에서 함수형 컴포넌트 실행순서
렌더링 => 컴포넌트 함수 호출 => 모든 내부 변수 초기화
useMemo를 사용할때의 실행순서
렌더링 => 컴포넌트 함수 호출 => memoize된 함수 재사용
useMemo는 처음 계산된 값을 메모리에 저장해서 컴포넌트가 재랜더링되어도
함수를 다시 호출하지 않고 메모리에 저장되어 있는 계산된 값을 재사용할 수 있게 해준다.
//useMemo 구조
const value = useMemo(() => {
return calculate();
},[item])
첫 번째 인자로 콜백 함수, 두 번째 인자로 의존성 배열
의존성 배열 안에있는 값이 업데이트 될 때 콜백 함수를 다시 호출, 콜백 함수 반환값으로 메모리에 저장된 값을 업데이트
빈 배열을 넣는다면 마운트 될 때에만 값을 계산하고 그 이후엔 계속 memoization된 값을 꺼내와 사용한다.
// useMemo 예제
import { useMemo, useEffect, useState } from "react";
function App() {
const [number, setNumber] = useState(1);
const [isKorea, setIsKorea] = useState(true);
// 1번 location
// const location = {
// country: isKorea ? "한국" : "일본"
// };
// 2번 location
const location = useMemo(() => {
return {
country: isKorea ? '한국' : '일본'
}
}, [isKorea])
useEffect(() => {
console.log("useEffect 호출!");
}, [location]);
return (
<header className="App-header">
<h2>하루에 몇 끼 먹어요?</h2>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<hr />
<h2>내가 있는 나라는?</h2>
<p>나라: {location.country}</p>
<button onClick={() => setIsKorea(!isKorea)}>Update</button>
</header>
);
}
export default App;
1번 예제에서는 number state가 바뀔때 location이 바뀌었다고 인식해서 useEffect가 실행된다.
2번 예제에서는 number state가 바뀔때 useEffect가 실행되지 않고 location이 바뀔때만 실행된다.
number state를 변경해도 useEffect가 실행되는 이유는 자바스크립트 객체 특성과 관련있다.
자바스크립트에선 객체 값을 저장할때 주소값으로 저장하는데, number state가 바뀌면 App 컴포넌트가 재호출 되면서 location의 주소값도 바뀌게 된다. 따라서 location이 바뀌었다고 인식하고 useEffect가 실행되는 것이다.
이러한 현상은 2번 location 예제처럼 useMemo 훅을 통해 방지할 수 있다.
useMemo 주의할 점
불필요한 값까지 useMemo를 써버리면 메모리를 낭비할 수 있다.
잦은 변동이 있는 경우 오히려 성능이 악화될 수 있다.
useCallback도 useMemo처럼 콜백함수를 인자로 넣고, 함수가 리턴하는 값을 memoization 해주는 최적화 훅이다. useMemo와의 차이점이라면 useMemo는 값을 memoization 하지만 useCallback은 콜백함수 자체를 memoization 한다는 것이다.
쉽게 얘기하면 이미 메모이제이션된 콜백 함수, 이미 생성된 함수를 반환하는 훅이다.
const calculate = useCallback((num) => {
return num + 1;
}, [item])
구조도 다음과 같이 useMemo와 똑같다. 다른점은 콜백 함수 자체를 memoization한다는 것이다.
컴포넌트가 다시 렌더링이 되더라도 함수가 초기화 되는 것을 막을 수 있다.
useCallback을 사용하면 컴포넌트가 다시 렌더링 되더라도, 해당 함수가 의존하고 있는 값들이 바뀌지 않는다면 함수를 새로 생성하지 않고 기존 함수를 계속 반환한다.
function Componenet(){
const calculate = useCallback((num) => {
return num + 1;
}, [item])
return <div>{value}</div>
}
컴포넌트가 처음 랜더링 될 때에만 함수 객체를 만들어서 초기화해주고, 이후 랜더링할 때는 calculate 변수가 새로운 함수 객체를 다시 할당받지 않고,
이전에 이미 할당받은 함수 객체를 계속해서 가지고 있는채로 재사용한다는 것이다.
import { useEffect, useState } from "react";
function App() {
const [number, setNumber] = useState(0);
const someFunction = () => {
console.log(`someFunc: number : ${number}`);
return;
};
// 의존성 배열에 someFunction
useEffect(() => {
console.log("📌 someFunction 이 변경되었습니다.");
}, [someFunction]);
return (
<div>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<br />
<button onClick={someFunction}>Call someFunc</button>
</div>
);
}
export default App;
state가 변경되면 컴포넌트가 재랜더링되는데, App 컴포넌트가 재랜더링되면 App 컴포넌트 안에 있는 함수 객체의 주소가 바뀌기 때문에 다르다고 인식되기 때문에 useEffect가 실행되게 된다. 그래서 위의 코드에서 Call someFunc 버튼을 클릭하지도 않고 그냥 숫자만 바꿔줬음에도 someFunction을 의존성배열로 가지는 useEffect 가 실행되는 것이다.

사진으로 보면 다음과 같이 변경된다.
컴포넌트를 렌더링 할 때마다 함수를 새로 선언하는 것은 성능상 큰 영향을 끼치지 않는다. 따라서, 모든 함수마다 useCallback을 사용하는 것은 큰 의미가 없고, 오히려 유지 보수를 어렵게 하거나 성능을 해칠 수 있다. useCallback의 의미있는 사용법을 알기 위해서는 자바스크립트의 함수 동등성에 대해서 알아야 한다.
자바스크립트에서 함수는 객체로 취급이 되기때문에, 함수를 동일하게 만들어도 메모리 주소가 다르면 다른 함수로 간주한다. 바로 메모리 주소에 의한 참조 비교가 일어나기 때문인데, 콘솔창에서 아래와 같이 동일한 코드의 함수를 작성하고 === 연산자로 비교를 해보면 false가 반환된다.
> const add1 = () => x + y;
undefined
> const add2 = () => x + y;
undefined
> add1 === add2
false
이 경우, useCallback을 이용해 함수를 특정 조건이 변경되지 않는 이상 재생성하지 못하게 제한하여 함수 동등성을 보장할 수 있다. (만약 리액트가 함수가 동등하지 않다고 판단한다면 상황에 따라 성능이 악화되거나, 무한루프에 빠지는 등의 문제를 겪을 수 있다.)
함수의 동등성이 문제가 되는 예시는 다음과 같다.
import React, { useState, useEffect } from "react";
function Profile({ id }) {
const [data, setData] = useState(null);
const fetchData = () =>
fetch(`https://test-api.com/data/${id}`)
.then((response) => response.json())
.then(({ data }) => data);
useEffect(() => {
fetchData().then((data) => setData(data));
}, [fetchData]);
// ...
}
fetchData는 함수이기 때문에, id 값과 관계없이 컴포넌트가 랜더링 될 때마다 새로운 참조값으로 변경된다. 함수가 변경됐으니까 매번 useEffect가 실행되어 재랜더링 될것이고, 무한루프에 빠지게 된다.
이 문제를 해결하려면 useCallback을 사용해 함수의 동등성을 유지하면 된다.
import React, { useState, useEffect } from "react";
function Profile({ id }) {
const [data, setData] = useState(null);
const fetchData = useCallback(
() =>
fetch(`https://test-api.com/data/${id}`)
.then((response) => response.json())
.then(({ data }) => data),
[id]
);
useEffect(() => {
fetchData().then((data) => setData(data));
}, [fetchData]);
// ...
}
이렇게 useCallback 훅을 사용하면, fetchData 함수의 참조값을 동일하게 유지시킬 수 있다. 따라서 useEffect의 의존성 배열 값에 있는 fetchData 함수는 id 값이 변경되지 않는한 재호출 되지 않는다.
참고자료
https://velog.io/@khy226/useMemo%EC%99%80-useCallback-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0