특징
2.
순수성
컴포넌트 함수는 왜 순수하게 유지해야 하는 걸까?
•
사이드 이펙트 허용: 이벤트 핸들러의 경우는.
state
hook
•
◦
훅은 순서대로 호출되기 때문에 if 조건문 안에서 쓰이면 예상치 못한 결과를 얻을 수 있음. 그래서 조건문 안에서는 쓰이면 안됨
렌더링
•
React에서 컴포넌트 함수를 호출하는 것.
•
화면 업데이트의 4단계: 1. 트리거 2. 렌더링 3. 커밋(DOM 노드 변경) 4. 페인팅
스냅샷으로서의 state
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
JavaScript
복사
•
버튼 클릭 시 number 값은 3이 아닌 1.
•
렌더링 당시의 state 값(여기서는 0)이 사용되기 때문에 이런 현상이 발생. setNumber(0 + 1)과 같음.
•
여러번 업데이트 하고 싶다면? 아래처럼 콜백을 전달하면 됨. state 업데이트 큐에 들어가서 순서대로 처리됨.
setNumber(n => n + 1)
setNumber(n => n + 1)
setNumber(n => n + 1)
// state = 3
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
// 이 경우는 42가 렌더링 됨.
JavaScript
복사
•
state 변수의 값은 이벤트 핸들러의 코드가 비동기적이더라도 렌더링 내에서 절대 변경되지 않습니다.
•
React는 렌더링의 이벤트 핸들러 내에서 state 값을 “고정”으로 유지합니다.
객체 state 업데이트하기
•
spread 연산자는 얕은 복사라 한 depth 밖에 복사가 안된다는 걸 기억하기
•
코드가 길어진다면 Immer를 사용하기. (객체의 property에 접근하는 코드를 짤 수 있다. 이 코드는 이전 state를 변경하지 않는다.)
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
JavaScript
복사
◦
immer가 작동하는 원리
Immer가 제공하는 draft는 Proxy라고 하는 아주 특별한 객체 타입으로, 당신이 하는 일을 “기록” 합니다. 객체를 원하는 만큼 자유롭게 변경할 수 있는 이유죠! Immer는 내부적으로 draft의 어느 부분이 변경되었는지 알아내어, 변경사항을 포함한 완전히 새로운 객체를 생성합니다.
◦
Immer는 업데이트 핸들러를 간결하게 관리할 수 있는 좋은 방법이며, 특히 state가 중첩되어 있고 객체를 복사하는 것이 중복되는 코드를 만들 때 유용합니다.
state 관리
state 관리
1.
derived state 파생 데이터 같은 경우 중복으로 state를 선언하지 않고 기존 데이터에서 계산하여 사용한다.
2.
중첩된 state도 피하기: 업데이트 하고 싶은 프로퍼티 외에 다른 프로퍼티도 다시 써줘야 하며, 변경사항이 없는 프로퍼티까지 모든 객체의 복사본을 만든다.
reducer
1.
여러 이벤트 핸들러에서 state 업데이트가 일어나는 경우 useReducer를 사용해 로직을 간결화 해보자.
2.
https://github.com/immerjs/use-immer#useimmerreducer useImmerReducer 사용 가능
escape hatch
1.
탈출구 → react 인터페이스 외부의 인터페이스를 사용하는 방법을 알아보자.
ref
1.
2.
ref는 React가 추적하지 않는 컴포넌트의 비밀 주머니와 같습니다. 예를 들어 ref를 사용하여 컴포넌트의 렌더링 출력에 영향을 주지 않는 timeout ID, DOM 엘리먼트 및 기타 객체를 저장할 수 있습니다.
•
dom 엘리먼트를 조작하고 싶은 경우. 조작하고 싶은 컴포넌트의 ref prop에 ref 객체를 전달하면, react가 ref.current 값에 dom 엘리먼트를 설정해준다.
3.
forwardRef: 다른 컴포넌트의 dom 노드에 접근할 수 있게 해준다.
•
몇몇 상황에서는 노출된 기능을 제한하고 싶을 수 있는데, 이 때 useImperativeHandle을 사용합니다.
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
// MyInput의 ref prop을 통해 상위 컴포넌트의 ref 객체를 전달받을 수 있다.
export default function MyForm() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
JavaScript
복사
4.
React가 관리하는 DOM 노드를 직접 바꾸려 하지 마세요. (ex. 돔 노드 삭제 행위)
effect
1.
Effect는 커밋이 끝난 후에 화면 업데이트가 이루어지고 나서 실행됩니다. 이 시점이 React 컴포넌트를 외부 시스템(네트워크 또는 써드파티 라이브러리와 같은)과 동기화하기 좋은 타이밍입니다.
2.
의존성 배열이 없는 경우, 모든 렌더링 후에 실행된다.
•
의존성 배열이 있는 경우
◦
[]: 마운트된 후에 한번만 실행된다. 개발 모드에서는 react에서 자체적으로 두번 실행한다. (사용자가 뒤로 가기해서 다시 한번 렌더링되는 경우에서 예상치 못한 버그가 발생하는지 확인시키려고) 프로덕션에서는 두번 실행되지 않음.
◦
[의존성]: 마운트 된 후 실행 + 의존하는 값이 변경될 때마다 실행된다.
useEffect(() => {
// 모든 렌더링 후에 실행됩니다
});
useEffect(() => {
// 마운트될 때만 실행됩니다 (컴포넌트가 나타날 때)
}, []);
useEffect(() => {
// 마운트될 때 실행되며, *또한* 렌더링 이후에 a 또는 b 중 하나라도 변경된 경우에도 실행됩니다
}, [a, b]);
JavaScript
복사
// 이 코드는 렌더링 중에 돔 노드를 조작하려고 시도하기 때문에 X
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
if (isPlaying) {
ref.current.play(); // 렌더링 중에 이를 호출하는 것이 허용되지 않습니다.
} else {
ref.current.pause(); // 역시 이렇게 호출하면 바로 위의 호출과 충돌이 발생합니다.
}
return <video ref={ref} src={src} loop playsInline />;
}
// useEffect로 감싸서 화면 업데이트 이후에 실행되도록 하자.
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
JavaScript
복사
3.
기본적으로 Effect는 모든 렌더링 이후에 실행되기 때문에 effect안에서 즉시 state를 설정하면 무한 루프를 만들어낼 수 있다. (state 변경 → 리렌더링 → effect 실행 → state 변경 ….)
•
때문에 아래와 같은 코드는 위험쓰.
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
JavaScript
복사
4.
ref를 의존성 배열에 포함하지 않아도 되는 이유
ref 객체가 *안정된 식별성(stable identity)*을 가지기 때문입니다. React는 동일한 useRef 호출에서 항상 같은 객체를 얻을 수 있음을 보장합니다. 이 객체는 절대 변경되지 않기 때문에 자체적으로 Effect를 다시 실행시키지 않습니다. 따라서 ref는 의존성 배열에 포함하든 포함하지 않든 상관없습니다. 포함해도 문제없습니다.
5.
effect에서 데이터를 가져오는 경우
•
서버에서 실행되지 않음 → 초기 서버사이드 렌더링된 html은 데이터가 없는 로딩 상태만 포함됨. 클라이언트 측에서 렌더링 이후에야 데이터를 로드하게 됨.
•
네트워크 폭포를 발생시킨다.
•
데이터를 미리 로드하거나 캐시하지 않음. 컴포넌트가 다시 마운트되면 데이터를 다시 가져와야 함.
→ 좋은 대안?
클라이언트 측 캐시를 사용하거나 구축하는 것을 고려하세요. 인기 있는 오픈 소스 솔루션으로는 React Query, useSWR 및 React Router 6.4+이 있습니다. 직접 솔루션을 구축할 수도 있으며 이 경우 Effect를 내부적으로 사용하면서 요청 중복을 제거하고 응답을 캐시하고 네트워크 폭포를 피하는 로직을 추가할 것입니다. (데이터를 사전에 로드하거나 데이터 요구 사항을 라우트)
6.
필요한 경우 클린업 함수를 리턴하자. (개발모드에서 두번 실행되는 경우에 대한 대응, 구독 취소 등등)
7.
데이터를 가져왔을 때 해당 데이터로 새 값을 계산하는 경우 → 렌더링될 때마다 새로 계산함. → 이 경우에는 useMemo를 사용하면 리렌더링이 되도 내부 함수가 재실행되지 않는다. 의존성 배열에 있는 값이 바뀌는 경우에는 실행된다.
hooks
useCallback
1.
언제 사용하느냐
•
useMemo, useCallback과 같은 memoization 훅은 자식 컴포넌트의 리렌더링을 최적화할 때 사용
•
커스텀 hook을 최적화할 때도 사용한다.
◦
커스텀 훅에서 반환하는 모든 함수는 useCallback으로 감싸는 게 좋다.
components
suspense
1.
suspense의 자식 요소가 로드되기 전까지 화면에 대체 ui를 보여준다.
2.
Suspense는 Effect 또는 이벤트 핸들러 내부에서 가져오는 데이터를 감지하지 않습니다.
API
lazy
1.
로딩 중인 컴포넌트가 렌더링될 때까지 지연시킬 수 있음
import { lazy } from 'react';
// ✅ 올바른 방법: lazy 컴포넌트를 컴포넌트 외부에 선언합니다.
const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));
JavaScript
복사
•
이 패턴을 사용하려면 임포트하려는 lazy 컴포넌트가 default export로 내보내져 있어야 합니다.
memo
•
memo를 사용하면 컴포넌트의 props가 변경되지 않은 경우 리렌더링을 건너뛸 수 있습니다.
•
일반적으로 부모 컴포넌트가 리렌더링 되어도 props가 변경되지 않았다면 리렌더링 되지 않습니다.