
- 이하 내용에서 렌더링은 렌더와 커밋 단계를 모두 포함하는 개념을 지칭합니다.
- 함수형 컴포넌트는 매번 함수를 실행해 렌더링을 수행한다
import { useState } from "react";
const [state, setState] = useState(initialState);
함수형 컴포넌트 내부에서 상태를 정의하고, 관리할 수 있게 해주는 훅
리액트의 렌더링과 상태
export default function App() {
const [, setState] = useState(0);
let state = "hello";
function handleButtonClick() {
state = "hi";
setState();
}
return (
<main>
<h1>{state}</h1>
<button onClick={handleButtonClick}>hi</button>
</main>
);
}
useState의 내부 구현 생각해보기
const MyReact = function(){
const global = {}
let index = 0
function useState(initialState) {
// 애플리케이션 최초 접근 시, 전체 states 배열 초기화
if (!global.states) {
global.states = []
}
// state: 현재 상태값 확인, 없으면 초깃값 설정
const currentState = global.states[index] || initialState
global.states[index] = currentState
// setter: index를 기억하는 즉시실행함수
const setState = (function () {
let currentIndex = index
return function (value) {
// 상태 업데이트
global.states[currentIndex] = value
// 이후 컴포넌트 렌더..
}
})()
}
// 각 state마다 새로운 index 할당
index += 1
return [currentState, setState]
}
게으른 초기화(lazy initialization)
const [count, setCount] = useState(() => Number.parseInt(window.localStorage.getItem(cacheKey)));
useState(함수)localStorage, sessionStorage에 대한 접근, 배열에 대한 접근(map, filter, find), 초깃값 계산을 위해 함수 호출이 필요한 경우useEffect(() => {}, [props, state]);
렌더링 때마다 의존성에 있는 컴포넌트의 값들을 활용해 동기적으로 부수 효과를 만드는 메커니즘
어떤 상태값과 함께 실행(props, state)되는지가 중요
첫 번째 인수로 부수 효과가 포함된 함수를, 두 번째 인수로 의존성 배열을 전달
클린업 함수
const [counter, setCounter] = useState(0);
function handleClick() {
console.log("event");
setCounter((prev) => prev + 1);
}
// 렌더링마다 실행
useEffect(() => {
console.log("Effect 1");
// 클린업 함수
return () => {
console.log("cleanup 1");
};
});
// counter가 변경될 때마다 실행
useEffect(() => {
console.log("Effect 2");
function addMouseEvent() {
console.log(counter);
}
window.addEventListener("click", addMouseEvent);
// 클린업 함수
return () => {
console.log("cleanup 2: ", counter);
window.removeEventListener("click", addMouseEvent);
};
}, [counter]);
return (
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+</button>
</>
);
실행 결과
// 초기 렌더링
Effect 1
Effect 2
-
// 리렌더링 (이벤트 발생)
event
cleanup 1
cleanup 2: 0
Effect 1
Effect 2
1
useEffect는 이전의 클린업 함수를 실행한 뒤에 콜백 실행
함수형 컴포넌트 리렌더링 시, 의존성 변화가 있었을 이전의 값을 기준으로 실행되어, 이전 상태를 청소 해 주는 개념
의존성 배열
[]: 초기 렌더링 이후 실행
값을 주지 않은 경우: 컴포넌트 렌더링 이후 매번 실행
useEffect(() => {
console.log("컴포넌트 렌더링");
});
useEffect 사용의 의미
// 직접 실행
function Component() {
console.log("렌더링됨");
}
// useEffect 사용
function Component() {
useEffect(() => {
console.log("렌더링됨");
});
}
useEffect는 컴포넌트의 렌더링이 완료된 이후에 실행되지만, 직접 실행은 컴포넌트가 렌더링되는 도중에 실행. 이 작업은 함수형 컴포넌트의 반환을 지연시키는 행위로, 성능에 악영향을 미칠 수 있다.useEffect의 구현
const MyReact = function () {
// 컴포넌트에서 훅 실행 순서 보장
const global = {};
let index = 0;
function useEffect(callback, dependencies) {
const hooks = global.hooks;
// 이전 훅 정보가 있는지 확인
let previousDependencies = hooks[index];
// 얕은 비교로 값을 비교해 변경 확인 ⭐️⭐️
let isDependenciesChanged = previousDependencies
? dependencies.some((value, idx) => !Object.is(value, previousDependencies))
: true;
// 변경 됐으면 콜백 함수 실행
if (isDependenciesChanged) {
callback();
}
// 현재 의존성 다시 훅에 저장
hooks[index] = dependencies;
index += 1;
}
return { useEffect };
};
Object.is)해 하나라도 변경 사항이 있으면 callback으로 선언한 부수 효과 실행useEffect 사용 시 주의할 점
eslint-disable-line react-hooks/exhaustive-deps 주석은 최대한 자제하라useEffect(() => {
console.log(props);
}, []); // eslint-disable-line react-hooks/exhaustive-deps[]가 필요한지 생각해보고, 부모 컴포넌트에서 실행되는 것을 고려해보기useEffect의 첫 번째 인수에 함수명을 부여하라.
useEffect(() => {
function logActiveUser() {
logging(user.id);
}
}, [user.id]);
거대한 useEffect를 만들지 마라.
불필요한 외부 함수를 만들지 마라.
useEffect(() => {
const controller = new AbortController()(async () => {
const result = await fetchInfo(id, { signal: controller.signal });
setInfo(await result.json());
})();
return () => controller.abort();
}, [id]);
왜 useEffect의 콜백 인수로 비동기 함수를 바로 넣을 수 없을까?
function Component() {
useEffect(() = {
let shoudlIgnore = false
async function fetchData() {
const response = await fetch('http://some.website.com')
const result = await response.json()
if (!shouldIgnore) {
setData(result)
}
}
fetchData()
return () => {
// setter 실행을 막을 수 있다.
shouldIgnore = true
}
})
}
const memoizedValue = useMemo(() => expensiveComputation(a, b), [a, b]);
렌더링마다 특정 함수를 새로 만들지 않고 재사용해 불필요한 리소스나 리렌더링을 방지
const ChildComponent = memo(({ name, onChange }) => {
// 렌더링 확인
useEffect(() => {
console.log(`rendering: child ${name}`);
});
return (
<>
<h1>{name}</h1>
<button onClick={onChange}>toggle</button>
</>
);
});
export default function App() {
const [status1, setStatus1] = useState(false);
const [status2, setStatus2] = useState(false);
const toggle1 = () => {
setStatus1(!status1);
};
const toggle2 = useCallback(() => {
setStatus2(!status2);
}, [status2]);
return (
<>
<ChildComponent name="1" onChange={toggle1} />
<ChildComponent name="2" onChange={toggle2} />
</>
);
}
useCallback의 구현
export function useCallback(callback, args) {
currentHook = 8;
return useMemo(() => callback, args); // 함수를 값으로 반환
}
useCallback과 useMemo의 차이
export default function App() {
const [status1, setStatus1] = useState(false);
const [status2, setStatus2] = useState(false);
const toggle1 = useMemo(() => {
return () => setStatus1(!status1); // 반환한 함수 값 자체를 메모
}, [status1]);
const toggle2 = useCallback(() => {
// 인수로 받은 함수를 메모
setStatus2(!status2);
}, [status2]);
return (
<>
<ChildComponent name="1" onChange={toggle1} />
<ChildComponent name="2" onChange={toggle2} />
</>
);
}
DOM에 접근하거나 렌더링을 발생시키지 않고 원하는 상태값을 저장
useState vs. useRef
공통점
차이점
useRef는 반환값인 객체의 current 값에 접근/변경 가능
값이 변해도 렌더링을 발생시키지 않는다.
export default function UseRef() {
const count = useRef(0);
function handleClick() {
console.log("current: ", count.current); // 0 1 2 3
count.current += 1;
}
// 그대로
return (
<>
<button onClick={handleClick}>{count.current}</button>
</>
);
}
함수 외부 값 선언 vs. useRef
사용
undefinedexport default function UseRef() {
const inputRef = useRef();
// 렌더링 실행 이전이므로 undefined
console.log(inputRef.current); // undefined
// 렌더링 실행 이후
useEffect(() => {
console.log(inputRef.current); // <input type="text">
}, [inputRef]);
return <input ref={inputRef} type="text" />;
}
렌더링을 발생시키지 않고 원하는 상태값 저장
usePrevious 훅: useState의 이전 값 저장
function usePrevious(value) {
const ref = useRef();
// value가 변경되면 그 값을 ref에 저장
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
export default function Component() {
const [counter, setCounter] = useState(0);
const previousCounter = usePrevious(counter);
function handleClick() {
setCounter((prev) => prev + 1);
}
return (
<>
<p>
{counter} {previousCounter}
</p>
<button onClick={handleClick}>Increase</button>
</>
);
}
useRef 구현
useRef(initialValue) {
currentHook = 5
return useMemo(() => ({ current: initialValue }), [])
use로 시작하여, 내부에 리액트 훅을 사용하고, 리액트 훅의 규칙을 따라야 한다.
function useFetch<T>(url: string, { method, body }: { method: string; body?: XMLHttpRequestBodyInit }) {
// 응답 결과
const [result, setResult] = useState<T | undefined>();
// 요청 중 여부
const [isLoading, setIsLoading] = useState<boolean>(false);
// 2xx 3xx으로 정상 응답인지 여부
const [ok, SetOk] = useState<boolean | undefined>();
// HTTP status
const [status, setStatus] = useState<number | undefined>();
useEffect(() => {
const abortController = new AbortController();
(async () => {
setIsLoading(true);
const response = await fetch(url, {
method,
body,
signal: abortController.signal,
});
setOk(response.ok);
setStatus(response.status);
if (response.ok) {
const apiResult = await response.json();
setResult(apiResult);
}
setIsLoading(false);
})();
return () => {
abortController.abort();
};
}, [url, method, body]);
return { ok, result, isLoading, status };
}
export default function App() {
// data fetching
const { isLoading, result, status, ok } = useFetch<Array<Todo>>("https://jsonplaceholder.typicode.com/todos", {
method: "GET",
});
useEffect(() => {
if (!isLoading) {
console.log("fetchResult >> ", status);
}
}, [status, isLoading]);
return (
<>
{ok
? (result || []).map(({ userId, title }, index) => (
<div key={index}>
<p>{userId}</p>
<p>{title}</p>
</div>
))
: null}
</>
);
}
컴포넌트의 렌더링 결과물에 영향을 미치는 공통된 작업을 처리
with으로 시작하는 이름
부수 효과를 최소화하도록 인수로 받는 컴포넌트의 props를 임의로 수정, 추가, 삭제하지 말아야 한다.
여러 개의 고차 컴포넌트로 컴포넌트를 감쌀 경우 복잡성이 커질 수 있으므로 고차 컴포넌트는 최소한으로 사용하는 것이 좋다.
자바스크립트의 일급 객체, 함수의 특징을 사용하므로 자바스크립트 환경에서 사용 가능
React.memo
고차 컴포넌트 사용해보기
interface LoginProps {
loginRequired?: boolean;
}
// 고차 컴포넌트: 컴포넌트를 받아 컴포넌트를 반환
function withLoginComponent<T>(Component: ComponentType<T>) {
// 인수로 받는 props는 그대로 사용
return function (props: T & LoginProps) {
const { loginRequired, ...restProps } = props;
if (loginRequired) {
return <>로그인이 필요합니다</>;
}
return <Component {...(restProps as T)} />;
};
}
const Component = withLoginComponent((props: { value: string }) => {
return <h3>{props.value}</h3>;
});
export default function App() {
// 로그인 관련 정보
const isLogin = true;
return <Component value="text" loginRequired={isLogin} />;
}
중복된 로직을 분리해 컴포넌트의 크기를 줄이고 가독성을 향상
사용자 훅이 필요한 경우
고차 컴포넌트를 사용해야 하는 경우
고차 컴포넌트를 사용해야 하는 경우
Consider providing a default key for dynamic children · Issue #1342 · facebook/react
Preserving and Resetting State – React
같은 위치의 같은 컴포넌트는 상태를 보존한다.
import { useState } from "react";
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? <Counter isFancy={true} /> : <Counter isFancy={false} />}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={(e) => {
setIsFancy(e.target.checked);
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = "counter";
if (hover) {
className += " hover";
}
if (isFancy) {
className += " fancy";
}
return (
<div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)}>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>Add one</button>
</div>
);
}
같은 위치에 다른 컴포넌트는 상태를 재설정한다.
import { useState } from "react";
export default function App() {
const [isPaused, setIsPaused] = useState(false);
return (
<div>
{isPaused ? <p>See you later!</p> : <Counter />}
<label>
<input
type="checkbox"
checked={isPaused}
onChange={(e) => {
setIsPaused(e.target.checked);
}}
/>
Take a break
</label>
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = "counter";
if (hover) {
className += " hover";
}
return (
<div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)}>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>Add one</button>
</div>
);
}
같은 위치에 있는 상태 재설정하기