티스토리 뷰
1. Virtual DOM 가상 돔
일단 DOM에 대해 먼저 알아야 한다.
DOM - Document Object Model
요소를 담은 것이 문서
돔은 트리구조로 요소를 노드로 표현한 것
(API 제공 -> 이걸로 DOM 조작, querySelector 등등)
Virtual DOM
실제 돔을 복사한 것. (JS객체 형태로 메모리상에 저장된다.)
그러나 요소를 조작할 순 없다. (API 제공하지 않기 때문)
리액트는 이 가상돔을 사용하여 빠른 업데이트를 할 수 있게 했다. (실제 돔을 조작하지 않기 때문)
React는 가상돔을 어떻게 활용하길래 빠른 업데이트를 하나?
리액트는 항상 두 개의 가상 돔을 갖고 있다.
- 렌더링 이전 객체 (변경 이전)
- 렌더링 이후에 보이게될 화면 구조를 나타내는 객체 (변경 이후)
리액트는 state가 변경되면 렌더링이 되는데, 브라우저가 그려지기 전에 가상돔을 생성한다.
즉, 업데이트 이전의 가상돔과 업데이트 이후의 가상돔을 비교하여 (- Diffing)
딱 그 부분만 실제 돔에 적용시킨다. (- Reconciliation: 재조정)
재조정이 효율적인 이유? 바로 Batch Update 때문
변경된 모든 엘리먼트를 일괄 적용한다. (Batch)
-> 빠르고 효율적이다.
- Diffing : 리액트가 가지고 있는 두 개의 가상 돔, 변경 이전 객체와 변경 이후 객체를 비교하여 어떤 부분이 바꼈는지 파악
- Reconciliation : Diffing을 통해 변경 부분을 파악하여 실제 DOM에 일괄 적용 (Batch Update)
2. memo
// 코드 첨부하기
3. 리액트 훅 깊게 살펴보기
함수형 컴포넌트가 상태를 사용하거나 클래스형 컴포넌트의 생명주기 메서드를 대체하는 등의 다양한 작업을 하기 위해 훅이 생겨났다.
3.1 useState
함수형 컴포넌트 내부에서 상태를 정의하고 관리할 수 있게 한다.
리액트는 클로저를 사용한 라이브러리다.
const [data, setData] = useState();
useState 내부에 선언된 setData 함수는 실행이 종료된 이후에도 지역변수인 state를 계속 참조한다.
게으른 초기화 (lazy initialization)
일반적으로 useState에서 기본값을 선언하기 위해 useState()인수로 원시값을 넣는 경우가 대부분이다.
그러나, 함수를 인수로도 넘겨줄 수 있다. 이를 게으른 초기화라고 한다.
게으른 초기화는 useState의 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용하라고 돼 있다.
이 게으른 초기화 함수는 오로지 state가 처음 만들어질 때만 사용된다.
// 일반적인 useState 사용 - 바로 값을 넣는다.
const [count, setCount] = useState(
Number.parseInt(window.localStorage.getItem(cacheKey))
);
// 게으른 초기화 - 함수를 실행하여 값을 반환한다는 것
const [count, setCount] = useState(() =>
Number.parseInt(window.localStorage.getItem(cacheKey))
);
언제 게으른 초기화를 사용하나요?
localStorage나 sessionStorage에 대한 접근, map, filter, find 같은 배열에 대한 접근, 혹은 초깃값 계산을 위해 함수 호출이 필요할 때와 같이 무거운 연산을 포함해 실행 비용이 많이 드는 경우에 게으른 초기화를 사용하면 좋다.
3.2 useEffect
내가 아는 useEffect의 정의
- 두 개의 인수를 받는다. 첫 번째는 콜백, 두 번째는 의존성 배열이다.
의존성 배열에 값이 변화되면 첫 번째 인수인 콜백을 실행한다. - 클래스형 컴포넌트의 생명주기 메서드와 비슷한 작동을 구현할 수 있다.
두 번째 의존성 배열에 빈 배열을 넣으면 mount 될 때만 실행된다. - useEffect는 클린업 함수를 반환한다. 이 함수는 unMount 될 때 실행된다.
그러나 위 정의들은 어느 정도 옳지만 완전히 정확한 것도 아니다.
정확한 정의는,
useEffect는 애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 매커니즘이다.
그리고 그 부수 효과가 ‘언제’ 일어나는지보다 어떤 상태값과 함께 실행 되는지 살펴보는 것이 중요하다.
3.2.1 useEffect는 의존성 배열이 변경된 것을 어떻게 아는 것일까?
여기서 기억할 것은 함수형 컴포넌트는 매번 함수를 실행해 렌더링을 수행한다.
렌더링할 때마다 의존성에 있는 값을 보면서 이 의존성의 값이 이전과 다른 게 하나라도 있으면 부수 효과를 실행하는 평범한 함수일 뿐이다.
state와 props의 변화 속에서 일어나는 렌더링 과정에서 실행되는 부수 효과 함수이다.
클린업 함수의 목적
useEffect 내에서 반환되는 클린업 함수는 정확히 무엇이고 어떤 일을 할까?
일반적으로, 이벤트를 등록하고 지울 때 사용한다고 알려져 있다.
로그를 살펴보면 클린업 함수는 이전 count 값, 즉 이전 state를 참조해 실행한다.
클린업 함수는 새로운 값과 함께 렌더링 뒤에 실행이 된다. 여기서 중요한 것은 클린업 함수는 새로운 값을 기반으로 렌더링 뒤에 실행되지만, 변경된 값을 읽는 것이 아닌 함수가 정의됐을 당시에 선언 됐던 이전 값을 보고 실행된다는 것.
=> 즉, 특정 이벤트의 핸들러가 무한히 추가되는 것을 방지하여 이전에 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가했던 것이다.
엄연히 따지고보면, unMount 개념과는 조금 차이가 있다.
- 언마운트 : 특정 컴포넌트가 DOM에서 사라지는 것을 의미한다.
- 클린업 함수 : (언마운트라기보다는) 함수형 컴포넌트가 리렌더링됐을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는, 말 그대로 이전 상태를 청소해주는 개념
의존성 배열
- [] 빈 배열 : 비교할 것이 없으니 최초 렌더링 직후에 실행된 다음부터는 더 이상 실행되지 않음.
-
eslint-disable-line react-hooks/exhaustive-deps 주석은 최대한 자제하라.
eslint-disable-line react-hooks/exhaustive-deps 이 주석을 사용하여 react-hooks/exhaustive-deps 룰에서 발생하는 경고를 무시하는데, 이는 useEffect 인수 내부에서 사용하는 값 중 의존성 배열에 포함돼 있지 않은 값이 있을 때 경고를 발생시킨다.useEffect는 반드시 의존성 배열로 전달한 값의 변경에 의해 실행되어야 하는 훅이다.
의존성 배열 없이 콜백 함수 내부에서 특정 값을 사용한다는 것은, 부수 효과가 실제로 관찰해서 실행돼야 하는 값과는 별개의 동작을 한다는 의미. -
useEffect의 첫 번째 인수에 함수명을 부여하라
// 공식 문서에서도 첫 번째 인수로 익명 함수를 넘겨주고 있다. useEffect(() => { console.log(user.id); }, [user.id]) // 복잡성이 낮다면 상관없겠다만, 기명 함수로 바꾸는 것이 좋다. // 변수에 적절한 이름을 붙이는 이유? 해당 변수가 왜 만들어졌는지 파악하기 위함이다. (목적성 부여) useEffect( function logActiveUser() { logging(user.id) }, [user.id] ) -
거대한 useEffect를 만들지 마라
useEffect는 의존성 배열을 바탕으로 렌더링 시 의존성이 변경될 때마다 부수 효과를 실행한다.
이 부수 효과의 크기가 커질수록 애플리케이션의 성능에 악영향을 미친다.- 최대한 간결하고 가볍게 유지한다.
- 부득이하게 큰 useEffect를 만든다면, 여러 개의 useEffect로 분리하는 것이 좋다.
- 만약, 불가피하게 의존성 배열에 여러 변수가 들어가야 한다면, 최대한 useCallback, useMemo로 사전에 정제한 내용들만 useEffect에 담는다.
-
불필요한 외부 함수 만들지 마라
(206p 참조)
🌈 왜 useEffect의 콜백 인수로 비동기 함수를 바로 넣을 수 없나?
-> useEffect의 경쟁 상태(race condition)
// ❌ - Error
useEffect(async () => {
// useEffect에 async 함수를 넘겨주면 아래와 같은 에러가 발생함
// Effect callbacks are synchronous to prevent race conditions.
// Put the async function inside:
const response = await fetch('http://some.data.com');
const result = await response.json();
setData(result)
}, [])
// ✅
useEffect(() => {
(async () => {
const response = await fetch('http://some.data.com');
const result = await response.json();
setData(result)
})()
}, [])
// 혹은 flag 변수를 둔다.
useEffect(() => {
let shouldIgnore = false;
async function fetchData() {
const response = await fetch('http://some.data.com');
const result = await response.json();
if (!shouldIgnore) setData(result)
}
fetchData();
return () => {
shouldIgnore = true;
}
}, [])
3.3 useMemo
비용이 큰 연산에 대한 결과를 저장(메모이제이션)해 두고, 이 저장된 값을 반환하는 훅이다.
값 뿐 아니라 컴포넌트도 가능하다.
import React, { useEffect, useMemo, useState } from "react";
function ExpensiveComponent({ value }) {
useEffect(() => console.log("rendering!"));
return <span>{value + 1000}</span>;
}
const Test = () => {
const [value, setValue] = useState(10);
const [, triggerRendering] = useState(false);
// 컴포넌트의 props를 기준으로 컴포넌트 자체를 메모이제이션 했다.
const MemoizedComponent = useMemo(
() => <ExpensiveComponent value={value} />,
[value]
);
function handleChange(e) {
setValue(e.target.value);
}
function handleClick() {
triggerRendering((prev) => !prev);
}
return (
<div>
<input value={value} onChange={handleChange} />
<button onClick={handleClick}>렌더링발생</button>
{MemoizedComponent}
</div>
);
};
export default Test;
여기서 triggerRendering으로 렌더링을 강제로 발생시켰지만, MemoizedComponent는 리렌더링되지 않는다.
value가 변경되지 않는 한 다시 계산되는 일은 없을 것이다.
=> useMemo 등 메모이제이션을 활용하면 무거운 연산을 다시 수행하는 것을 막을 수 있다는 장점이 있다.
3.4 useCallback
인수로 넘겨받은 콜백 자체를 기억한다.
특정 함수를 새로 만들지 않고 재사용한다는 의미이다.
어떻게 재사용하나?
import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
const ChildComponent = memo(({ name, value, onChange }) => {
useEffect(() => {
console.log("rendering!", name);
});
return (
<>
<h1>
{name} {value ? "켜짐" : "꺼짐"}
</h1>
<button onClick={onChange}>toggle</button>
</>
);
});
const Test = () => {
const [status1, setStatus1] = useState(false);
const [status2, setStatus2] = useState(false);
// 이렇게 하면 하나만 변해도 둘 다 렌더링이 된다.
// const toggle1 = () => {
// setStatus1(!status1);
// };
// const toggle2 = () => {
// setStatus2(!status2);
// };
const toggle1 = useCallback(
function toggle1() {
setStatus1(!status1);
},
[status1]
);
const toggle2 = useCallback(
function toggle1() {
setStatus2(!status2);
},
[status2]
);
return (
<div>
<ChildComponent name="1" value={status1} onChange={toggle1} />
<ChildComponent name="2" value={status2} onChange={toggle2} />
</div>
);
};
export default Test;
3.5 useRef
- useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
- useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.
import React, { useEffect, useRef } from "react";
const Test = () => {
const inputRef = useRef();
// 이때는 미처 렌더링이 실행되기 전(반환되기 전)이므로 undefined를 반환한다.
console.log(inputRef.current); // undefined
useEffect(() => {
console.log(inputRef.current); // HTMLInputElement { '__reactFiber~'}
}, [inputRef]);
return <input ref={inputRef} type="text" />;
};
export default Test;
useRef는 최초에 넘겨받은 기본값을 가진다.
명심할 부분은, useRef의 최초 기본값은 return 문에 정의해둔 DOM이 아니고!
useRef()로 넘겨받은 인수라는 것이다.
useRef가 선언된 당시에는 아직 컴포넌트가 렌더링되기 전이라 return으로 컴포넌트의 DOM이 반환되기 전이므로 undefined이다.
(혹시 틀린 정보가 있다면 댓글로 알려주세요!)
- 이 글은 옵시디언에서 작성되었습니다.
'[개발] > React' 카테고리의 다른 글
| [React] Context (0) | 2023.12.15 |
|---|---|
| [React] lazy (0) | 2023.07.05 |
| 비동기 요청 라이브러리 (0) | 2023.06.27 |
| useReducer (0) | 2023.06.27 |
| 최적화 (0) | 2023.06.20 |