개요
리액트를 공부하다 보면 메모이제이션을 접하게 된다. React.memo
는 컴포넌트, useMemo
는 값, useCallback
은 함수를 메모이제이션해서 최적화한다는 건 다들 알고 있지만 "섣부른 최적화는 독이다"라는 말도 있듯 정확히 어디에 해야 하는지는 좀 대답하기 애매한 것 같다. 당연히 비용이 비싼 연산은 메모이제이션 하면 좋겠지만 그 기준은? 리렌더링이 자주 일어나는 컴포넌트는 뭘까? 모든 부분에서 전부 확인해 보면서 메모이제이션을 해야 할지 확인해야 하나? 메모이제이션 하는 데에도 추가 비용이 든다는데 그건 어느 정도 일까? 흠,, 헷갈린다. 나름의 기준을 잡아보자.
리액트 렌더링 과정
먼저 리액트가 어떻게 화면을 그리고, 언제 리렌더링이 되는지 간단하게만 알아보자.
JSX 코드를 Babel로 트랜스파일링
jsx는 표준 문법이 아니다. 브라우저의 자바스크립트 엔진은 기본적으로 jsx 문법을 이해하지 못하므로 Babel과 같은 도구로 트랜스파일링 해주어야 한다.
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
이런 jsx 코드는 리액트 프로젝트를 설치하면 들어있는 빌드 도구의 빌드 프로세스 중에 자동으로 아래의 코드로 변환된다.
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React 엘리먼트를 Fiber 노드로 변환
Fiber 노드는 React 16 버전부터 도입된 자료구조이다. React 16 이전의 Stack Reconciler에서는 동기적으로 재조정이 이루어져 렌더링 작업이 시작되면 완료될 때까지 중단될 수 없었고, 우선순위 개념이 없어 모든 업데이트가 동일한 우선순위로 처리되었다. React 16 이후 Fiber Reconciler가 도입된 이후 비동기적 렌더링을 지원하고, 중요한 작업이 있을 때 렌더링 작업을 중단할 수 있으며, 업데이트에 우선순위를 부여하는 등 다양한 기능들을 지원하게 되었다. 간단히 말해, Fiber 노드는 실제 DOM이랑 대응되며, 리액트 내부적으로 일어나는 재조정 과정을 위한 정보들을 추가적으로 담고있는 자료구조이다. 이 정도로만 알고 넘어가자.

Render Phase - Virtual DOM 생성
이후 React는 Fiber 노드를 사용하여 Virtual DOM 트리를 구성하고 저장해 둔다.

초기 렌더링 시에는 그냥 올라간다. 이후에 컴포넌트의 상태나 props가 변경되었을 때, 트리 전체를 다 변경하지 않고 이전의 Virtual DOM과 변경된 Virtual DOM을 비교하여 변경된 부분만 교체해 준다. 이 과정을 Diffing
이라고 하며, 찾아낸 변경점을 새로운 Virtual DOM에 적용하는 과정을 재조정(Reconciliation)
이라고 한다.
Virtual DOM이 항상 좋을까?
Virtual DOM은 메모리 공간을 차지하고, 변경사항을 찾는 diffing 과정 역시 CPU를 활용한다. 따라서 DOM트리가 복잡하고, 상태 변경도 빈번하게 일어나는 대규모 애플리케이션에서는 변경된 부분만 찾아주는 Virtual DOM이 더 빠르지만, 간단한 애플리케이션에서는 오히려 오버헤드가 발생할 수 있다.
Commit Phase
렌더 단계에서 계산된 변경사항(Virtual DOM 트리)을 실제 DOM에 적용한다.
최적화에 드는 비용
React.memo
React.memo
부터 알아보자. 위에서 설명했듯 컴포넌트 리렌더링은
- props가 변경되거나
- state가 변경되거나
- 부모 컴포넌트가 리렌더링 되거나
의 경우에 발생한다. 상태나 props가 변경되는 경우는 당연히 리렌더링이 일어나는 경우가 맞으므로 두 경우는 memo가 불필요하다.
그러나 부모 컴포넌트가 리렌더링 되었는데 자식에게 전달하는 props가 변경되지 않으면, 자식 컴포넌트는 변경사항이 없음에도 리렌더링 된다. 이런 경우에 React.memo
를 통한 최적화를 고려해 볼 수 있다. 이 경우 memo에 드는 비용은 뭐가 있을까?
생각해 보면, 다음과 같은 추가적인 비용이 필요할 것 같다.
- props를 메모리에 저장해 두는 비용
- 얕은 비교를 통해 props가 변경되었는지 확인하는 비용
- 컴포넌트를 메모이제이션하는 비용
메모이제이션을 하면 이전 렌더링 결과물을 저장해 두고, 리렌더링 할 필요가 없다면 이전 결과물을 재사용한다. 근데 이 과정을 어디서 본 것 같지 않나? 맞다. 이 작업은 앞에서 본 재조정 알고리즘이고 기본적으로 이전 렌더링 결과를 저장하고 있다. 즉, memo에 실제로 지불하는 비용은 props에 대한 얕은 비교뿐이다. 물론 props가 크고 복잡해진다면 이 비용도 커지겠지만, diffing 등 렌더링 과정 전체에서 비용이 발생하고 자식 컴포넌트도 리렌더링 되어 발생하는 비용에 비하면 미미한 것 같다.
useMemo, useCallback
useCallback
은 내부적으로 useMemo
를 사용하여 구현되어 있다. 둘 다 값/함수를 저장하는 비용, 의존성 배열을 저장하는 비용, 의존성 배열을 비교하는 비용이 추가적으로 발생한다. useMemo
는 계산 비용이 많이 드는 연산을 수행할 때, 객체의 참조가 동일해야 할 때 사용해야 하고, useCallback
은 마찬가지로 함수의 참조가 동일해야 할 때 사용해야 한다. 아래에서 자세히 알아보자.
최적화가 필요한 경우
React.memo - 상위 컴포넌트가 리렌더링 되었을 때 하위 컴포넌트에 전달되는 props가 변경되지 않은 경우
const GrandChild = () => {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return (
...
);
};
const MemoizedGrandChild = React.memo(() => {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return (
...
);
});

부모, 자식 컴포넌트가 리렌더링 되고 있는데 손자 컴포넌트는 변경사항이 없는데도 리렌더링 되고 있다. 이런 상황에 손자 컴포넌트에 React.memo
를 씌워주면

다음과 같이 불필요한 리렌더링을 하지 않는다.
이는 Context를 사용할때도 마찬가지다. 자식이 Context를 사용중일 때 Context가 변경되면 자식 컴포넌트가 리렌더링 되고, 손자는 Context를 사용하지 않음에도 리렌더링 된다. 이때도 마찬가지로 손자 컴포넌트에 React.memo
를 씌워줌으로써 불필요한 리렌더링을 방지할 수 있다.
useMemo - 계산 비용이 많이 드는 연산
const heavyCalculation = (num) => {
let result = 0;
for (let i = 0; i < 500000; i++) {
result += Math.random() * num;
}
return result.toFixed(2);
};
useEffect(() => {
const start = performance.now();
const result = heavyCalculation(inputValue);
const end = performance.now();
const executionTime = end - start;
setNormalResult(result);
setNormalFuncCalls((prev) => prev + 1);
setNormalFuncTime((prev) => prev + executionTime);
}, [inputValue, counter]);
const memoizedResult = useMemo(() => {
const start = performance.now();
const result = heavyCalculation(inputValue);
const end = performance.now();
const executionTime = end - start;
setTimeout(() => {
setMemoFuncCalls((prev) => prev + 1);
setMemoFuncTime((prev) => prev + executionTime);
}, 0);
return result;
}, [inputValue]);
useMemo는 컴포넌트가 리렌더링되어도 의존성배열에 들어가는 값이 변하지 않으면 함수를 재실행하지 않고 이전 값을 그대로 사용한다. 당연히 계산 비용이 높고 의존성이 자주 변경되지 않는 경우에 적용하면 효과적이다.

useMemo / useCallback - 참조 동일성이 유지되어야 할 때
자바스크립트에서 기본 타입(number, string, boolean, null, undefined)을 제외한 모든 값은 '객체'이므로 함수 또한 객체로 표현되고, 객체의 모든 연산은 실제 값이 아닌 참조값으로 처리된다. 이 말은 기존 객체/함수와 새로 생성된 객체/함수의 내용물이 완벽히 동일하더라도 자바스크립트는 이를 다르게 인식한다는 뜻이다. 따라서 객체/함수를 props로 넘겨주거나 useEffect의 의존성 배열에 넣을 때 불필요한 리렌더링이나 effect 실행이 일어날 수 있다.
const userWithoutMemo = {
id: 1,
name: name,
role: "Admin",
};
const userWithMemo = useMemo(() => {
return {
id: 1,
name: name,
role: "Admin",
};
}, [name]);
...
<div>
<UserInfo user={userWithoutMemo} title="useMemo X" />
<UserInfo user={userWithMemo} title="useMemo O" />
</div>
위의 경우는 객체를 하위 컴포넌트로 전달할 때 useMemo를 사용하여 불필요한 리렌더링을 방지한다.

그러므로 객체를 직접 전달하지 않고 분해해서 전달하는 것이 좋은 패턴이다. 객체를 분해하여 필요한 속성만 개별적으로 전달하면 useMemo를 사용하는 비용도 필요 없이 불필요한 리렌더링을 없애줄 수 있다. 또한 이 방식은 컴포넌트의 의존성도 명확하게 보여주고 타입도 더 명확하게 정의할 수 있다.
// 객체를 분해하여 개별 속성으로 전달
<UserInfo
id={1}
name={name}
role="Admin"
title="Props 분해 전달"
/>
const handleWithoutCallback = () => {
console.log("메모이제이션 없는 함수");
};
const handleWithCallback = useCallback(() => {
console.log("메모이제이션된 함수");
}, []);
const FunctionEffect = ({ onAction, title }) => {
const renderCount = useRef(0);
const effectCount = useRef(0);
const totalRenderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
totalRenderCount.current += 1;
});
useEffect(() => {
effectCount.current += 1;
totalRenderCount.current += 1;
}, [onAction]);
return (
...
);
};
위의 코드는 useEffect의 의존성 배열에 함수를 포함시킨다. 이 경우 useCallback을 적용하지 않으면 다음과 같은 실행 흐름이 발생한다.
- 상위 컴포넌트 리렌더링
- 상위 컴포넌트에서 새 props를 받아 하위 컴포넌트 리렌더링
- 첫 번째 useEffect 실행
- 함수 참조가 변경되었으므로 의존성 배열의 변화 감지
- 두 번째 useEffect 실행
이로 인해 Effect가 2번씩 실행된다. 위의 경우에서 실제로 리렌더링을 유발하지는 않고 카운트만 올라가지만 만약 Effect 내부에서 setState와 같이 상태를 업데이트하는 함수를 호출한다면 컴포넌트도 실제로 한번 더 리렌더링 된다. 성능 저하와 의도치 예상치 못한 동작을 막기 위해 함수를 의존성 배열에 포함시킬 때는 useCallback을 사용하여 함수를 메모이제이션 해주자.

DOM에 접근할 땐 State 대신 Ref로 접근하기
이건 메모이제이션이랑은 다른 얘기이긴 한데 많이들 실수하는 부분인 것 같다. 앞에서 계속 말했듯, 컴포넌트의 리렌더링은 props와 state의 변화로 인해 발생한다. 그런데 다음과 같이 requestAnimationFrame으로 프레임마다 애니메이션이 변경되어야 하고, 그 변수를 상태로 저장한다면 어떻게 될까? 1초에 60번씩 상태가 변경되고, 그만큼 리렌더링이 계속 발생하며 매 프레임마다 Virtual DOM을 생성하고 비교해야 한다.
function StateAnimation() {
const [position, setPosition] = useState(0);
const [color, setColor] = useState("rgb(255, 0, 0)");
const [isAnimating, setIsAnimating] = useState(false);
const renderCountRef = useRef(0);
const animationRef = useRef(null);
...
useEffect(() => {
if (!isAnimating) return;
let startTime;
const animate = (timestamp) => {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const newPosition = Math.sin(elapsed * 0.003) * 50 + 50;
setPosition(newPosition);
const r = Math.round(Math.sin(elapsed * 0.002) * 127 + 128);
const g = Math.round(Math.sin(elapsed * 0.002 + 2) * 127 + 128);
const b = Math.round(Math.sin(elapsed * 0.002 + 4) * 127 + 128);
setColor(`rgb(${r}, ${g}, ${b})`);
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isAnimating]);
...
return (
<div className="p-6 border rounded-lg shadow-md">
...
<div
className="w-16 h-16 rounded-full absolute"
style={{
backgroundColor: color,
left: `${position}%`,
top: "50%",
transform: "translate(-50%, -50%)",
}}
></div>
...
</div>
);
}

이를 방지하기 위해 State가 아닌 Ref로 DOM에 직접 접근하여 React 렌더링 사이클을 우회해야 한다.
function RefAnimation() {
const [isAnimating, setIsAnimating] = useState(false);
const renderCountRef = useRef(0);
const boxRef = useRef(null);
const animationRef = useRef(null);
...
useEffect(() => {
if (!isAnimating || !boxRef.current) return;
let startTime;
const animate = (timestamp) => {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
if (boxRef.current) {
const newPosition = Math.sin(elapsed * 0.003) * 50 + 50;
const r = Math.round(Math.sin(elapsed * 0.002) * 127 + 128);
const g = Math.round(Math.sin(elapsed * 0.002 + 2) * 127 + 128);
const b = Math.round(Math.sin(elapsed * 0.002 + 4) * 127 + 128);
boxRef.current.style.left = `${newPosition}%`;
boxRef.current.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
}
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isAnimating]);
...
return (
<div className="p-6 border rounded-lg shadow-md">
...
<div
ref={boxRef}
className="w-16 h-16 rounded-full absolute"
style={{
backgroundColor: "rgb(255, 0, 0)",
left: "50%",
top: "50%",
transform: "translate(-50%, -50%)",
}}
></div>
...
</div>
);
}

애니메이션 등 빈번한 UI 업데이트가 필요한 경우 외의 다양한 상황에서도 Ref로 최적화할 수 있다. 드래그 앤 드롭 기능을 구현할 때도 마우스 위치를 계속 state로 업데이트할 필요가 없다. 마찬가지로 스크롤 이벤트 처리나 차트 업데이트와 같은 빈번한 인터랙션에서도 Ref를 사용해 DOM을 직접 조작하면 리액트의 렌더링 사이클을 거치지 않고 변경 사항을 즉시 적용할 수 있다. 또한 React 외부 라이브러리(D3.js, Three.js 등)와 통합할 때에도 Ref를 사용하면 라이브러리가 DOM을 직접 제어할 수 있으므로 충돌 없이 부드럽게 작동한다.
메모이제이션이 불필요한 경우
당연한 말인데, 자주 리렌더링되는 작은 컴포넌트나 계산 비용이 낮은 연산의 경우에는 메모이제이션을 적용하는 것이 오히려 메모리 사용과 비교 연산으로 인한 추가적인 비용을 발생시킨다.
불필요한 메모이제이션을 제거하기
Children Prop으로 자식 컴포넌트 전달하기
function ParentWrapper({ children }) {
const [wrapperCount, setWrapperCount] = useState(0);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return (
<>
...
{children}
</>
);
}
function Child() {
const [childCount, setChildCount] = useState(0);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return (
<div>
...
</div>
);
}
export default function App() {
return (
<div>
...
<ParentWrapper>
<Child />
</ParentWrapper>
</div>
);
}

메모이제이션을 하지 않았음에도, ParentWrapper의 상태가 변경되어도 Child가 리렌더링되지 않는다.
로컬 State를 사용하고, State를 상위 컴포넌트로 올리는 것 지양하기
function LocalStateExample() {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return (
<div>
...
<LocalForm />
</div>
);
}
function LocalForm() {
const [inputValue, setInputValue] = useState("");
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return (
<div>
...
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
...
<p>
현재 입력값: <span>{inputValue}</span>
</p>
</div>
);
}
function ParentStateExample() {
const [inputValue, setInputValue] = useState("");
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return (
<div>
...
<ParentForm
inputValue={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</div>
);
}
function ParentForm({ inputValue, onChange }) {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
return (
<div>
...
<input
type="text"
value={inputValue}
onChange={onChange}
className="w-full p-2 border rounded"
placeholder="여기에 입력하세요"
/>
...
<p>
현재 입력값: <span>{inputValue}</span>
</p>
</div>
);
}

form 등의 일시적인 state가 상위 컴포넌트 또는 전역 상태 라이브러리에 없도록 하자.
State를 업데이트하는 불필요한 useEffect 피하기
React 앱의 대부분의 성능 문제는 컴포넌트가 계속해서 렌더링되도록 하는 Effect에서 발생하는 일련의 업데이트로 인해 발생한다. Effect가 필요하지 않은 두 가지 일반적인 경우를 확인하여 이 문제를 피할 수 있다.
렌더링을 위해 데이터를 변환하는 경우
필터링된 목록을 화면에 보여준다고 가정해 보자. 일반적으로, 목록이 변경될 때 state를 업데이트하는 Effect를 작성하려고 할 것이다. state를 업데이트하면 React는 컴포넌트를 호출하여 화면에 호출할 내용을 계산하고 이러한 변경 사항을 DOM에 커밋하여 화면을 업데이트한다. 그 다음에 Effect가 실행된다. Effect로 인해 상태가 업데이트되면 전체 프로세스가 처음부터 다시 시작되는데, 이는 비효율적이다. 불필요한 렌더링을 피하려면 컴포넌트의 상위 수준에서 필터링된 목록을 내려주자.
사용자 이벤트를 처리하는 경우
사용자가 구매 버튼을 클릭하면 /api/buy에 POST 요청을 보내고 알림을 표시한다고 가정할 때, 사용자가 구매 버튼을 클릭한 것에 대한 이벤트는 이벤트 핸들러에서 처리해야 하는 일이다. Effect에서는 사용자가 어떤 버튼을 클릭했는지 등 사용자가 무엇을 했는지 알 수 없기 때문에 사용자 이벤트 처리를 Effect에서 처리하게 해서는 안 된다.
Effect의 불필요한 의존성 제거하기
메모이제이션 대신 객체나 함수를 컴포넌트 밖으로 빼는 것이 더 나을 수도 있다.
코드 변경을 반영하기 위해 Effect에 의존성을 주거나 작성한 의존성의 내용이 변경될 때마다 Effect를 다시 실행하도록 하는 것이 의도한 바가 맞는지 확인해야 한다. 때때로 Effect가 다시 실행될 필요가 없는 순간에도 의존성 목록의 내용 때문에 의도와 다르게 Effect가 재실행되는 경우가 있다.
조건에따라 Effect 안의 다른 부분을 재실행하고 싶은 경우
다음은 Effect에서 데이터를 가져오는 예 중 하나이다. country props에 따라 해당 country의 cities를 가져와 state에 저장하고, 사용자가 cities 중 특정 city를 선택하면 해당 city의 areas를 가져온다.
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// ⚠️ Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared
Effect에서 의존성으로 country, city를 모두 넣는 것이 맞는 것 같지만 이는 country는 그대로 유지된 채 city만 변경되어도 cities를 다 다시 불러오는 오류가 있다. 이 코드의 문제점은 두 가지의 서로 다른 항목이 하나의 Effect에 들어가 있다는 것으로, 이는 다음과 같이 분리해야 한다.
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ All dependencies declared
각 Effect는 독립적인 동기화 프로세스를 나타내도록 분리해야 한다. 하나의 Effect를 삭제해도 다른 Effect의 로직은 깨지지 않아야 한다. 중복이 우려된다면 반복적인 로직을 커스텀 훅으로 추출하여 이 코드를 개선할 수 있다.
변화에 반응하는 대신 의존성의 최신값을 읽기만 하고 싶은 경우
다음은 isMuted가 true가 아니면 사용자가 새 메시지를 수신할 때 사운드를 재생하고자 하는 예이다.
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]);
이 경우 isMuted가 변경될 때마다 Effect의 모든 일이 발생한다. 이 경우도 앞에서 설명한것처럼 useRef를 사용하여 usMuted를 관리할 수 있다.
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const isMuted = useRef();
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted.current) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId]);
의존성이 객체 또는 함수여서 의도하지 않게 너무 자주 변경되는 경우
앞에서는 함수가 의존성인 경우 useCallback을 사용하여 불필요하게 Effect가 실행되지 않는 예시를 다루었다.
다음 코드에서는 message state가 변경되면 options가 재생성되고, Effect는 options가 변경되었다고 감지하여 Effect 내부의 일을 실행한다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = {
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
이 경우 다음과 같이 객체 options를 Effect 내부에서 선언하여 Effect의 의존성에서 제거할 수 있다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
더 이상 message state가 변경되어도 불필요한 Effect가 작동하지 않는다.
마무리
이전에 프로젝트를 진행하면서 최적화를 진행했었다. 그땐 개선한다고 한거였는데, 지금 생각해 보면 처음부터 당연히 해야 하는 걸 안 하고 개선한다고 한 것 같다. . . 흠,, ㅋㅋㅋ ㅜㅜ
useCallback과 useRef의 경우에는 사용해야하는 부분이 명확한 것 같다. 메모이제이션을 사용하지 않고 최적화할 수 있는 경우들도 다루었다. useMemo와 React.memo의 경우 나름의 결론을 내려보자면, 의존성 배열이나 props를 저장하고 비교하는 리소스에 비해, 불필요하게 비싼 연산을 하거나 컴포넌트를 리렌더링 하는 비용이 훨씬 큰 것 같다. 당연히 전부 테스트를 해보고 최적화를 적용한다면 가장 좋겠지만, 그럴만한 여유가 없는 상황이라면 일단 의심 가는 부분에 전부 최적화를 적용해도 괜찮지 않을까 싶다.
참고
https://ko.legacy.reactjs.org/docs/introducing-jsx.html
JSX 소개 – React
A JavaScript library for building user interfaces
ko.legacy.reactjs.org
[번역] 리액트 렌더링 동작의 (거의) 완벽한 가이드 [A (Mostly) Complete Guide to React Rendering Behavior]
[번역] 리액트 렌더링 동작의 (거의) 완벽한 가이드
velog.io
https://d2.naver.com/helloworld/9223303
성능 하면 빠질 수 없는 메모이제이션, 네가 궁금해
성능 하면 빠질 수 없는 메모이제이션, 네가 궁금해
d2.naver.com
Quick Start – React
The library for web and native user interfaces
react.dev
'프론트엔드' 카테고리의 다른 글
오픈소스 프로젝트 메모리 누수 문제 해결하기 (1) | 2025.05.01 |
---|---|
웹 보안 취약점과 토큰 관리방식 알아보기 (0) | 2025.03.25 |
React와 비교를 통해 알아보는 React Native 아키텍처(+네이티브 모듈 적용하기) (0) | 2025.02.05 |
브라우저와 Node.js의 비동기 처리와 이벤트 루프 (0) | 2025.01.31 |
자바스크립트 깨알상식 (간단) (0) | 2025.01.26 |