3.1 리액트의 모든 훅 파헤치기
3.1.1 useState
함수 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅이다.
import { useState } from "react";
const [state, setState] = useState(initialState);
- initialState에는 state의 초깃값을 넘겨준다. 아무런 값이 없으면 undefined다.
- useState 훅의 반환 값은 배열이며, 배열의 첫번째 원소로 state 값 자체를 사용할 수 있고, 두번째 원소인 setState 함수를 사용해 해당 state의 값을 변경할 수 있다.
- useState의 클로저는 useState 내부에 선언된 함수(setState)가 함수의 실행이 종료된 이후에도(useState가 호출된 이후에도) 지역변수인 state를 계속 참조할 수 있다는 것을 의미한다.
게으른 초기화
- useState에 변수 대신 함수를 넘기는 것을 게으른 초기화(lazy initialization)이라고 한다.
- 리액트 공식문서에서는 useState의 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용하라고 돼있다.
- localStorage 또는 sessiong Storage에 대한 접근, map, filter, find 같은 배열에 대한 접근 혹은 초깃값 계산을 위한 함수 호출이 필요한 경우 사용하는 것이 좋다.
const [test, setTest] = useState(Number.parseInt(window.localStorage.getItem(cachekey))
3.1.2 useEffect
useEffect(()=>{
console.log('testingCode')
window.addEventListener('click',addMouseEvent);
return ()=>{
window.removeEventListener('click',addMouseEvent);
}
},[]);
useEffect 훅과 클린업 함수의 중요성
- useEffect 훅은 외부 시스템과 내부 컴포넌트의 동기화를 위해 자주 사용되며, 웹소켓연결 처리와 같은 예시로 설명될 수 있다 .
- 이 훅은 첫 번째 인자로 실행될 부수 효과가 포함된 함수를 받고, 두 번째 인자로 의존성 배열을 받아서, 배열 내부 값이 변경될 때마다 셋업 함수가 실행되는 구조로 되어 있다 .
- 컴포넌트 렌더링시마다 리액트가 의존성 배열의 값을 얕은 동등성 검사로 비교하여 변경이 발생하면 셋업 함수가 트리거 된다 .
- 클린업 함수는 useEffect의 셋업 함수에서 반환되며, 다음 렌더링전에 실행되어 이전 상태를 기반으로 동작하며, 이후 렌더링의 변경된 값은 참조하지 않는다 .
- 이러한 클린업 함수는 주로 리액트외부에서 관리되는 부수 효과를 정리할 때 사용되며, 이벤트 핸들러 제거나 웹소켓연결 종료 시 유용하게 활용될 수 있다 .
의존성 배열
- 의존성 배열은 useEffect의 실행을 관리하며, 배열의 요소가 변경될 경우 셋업 함수가 재실행된다. 만약 배열에 값이 없다면 최초 렌더링에서만 실행되고 이후 렌더링에서는 실행되지 않는다 .
- useEffect는 클라이언트 사이드 실행을 보장하는 수단이므로, 외부 코드에 놓일 경우 클라이언트에서 실행될 것이라는 보장을 제공하지 않는다 .
- 리액트 훅의 규칙을 준수하는 것이 중요하며, 의존성 배열의 값들을 반드시 deps에 포함시켜야 유즈 이펙트가 올바르게 작동한다 .
- useEffect의 복잡성을 줄이기 위해서는 함수명을 부여하고, 외부 함수가 아닌 내부에 선언하는 것이 좋다. 이 과정에서 메모이제이션을 사용하여 변경 사항을 추적하는 방법도 유효하다 .
- 비동기 함수를 useEffect내부에 직접 넣는 것을 피해야 하며, 이를 위해 별도의 비동기 함수를 생성하는 방식이 권장된다 .
useEffect의 비동기 처리와 발생할 수 있는 문제점
- useEffect를 사용할 때, 내부 코드의 복잡성이 증가하면 익명 함수로 인해 실행 조건을 파악하기 어려워지므로, 기명 함수 사용을 권장한다 .
- 리액티브한 값의 수가 많을 경우, 의도하지 않은 타이밍에 useEffect가 실행될 위험이 증가하므로, 이러한 값을 메모이제이션하여 변경 차이를 막는 방법도 고려할 수 있다 .
- useEffect내에서 외부 함수를 호출할 경우, 값 변경 추적이 복잡해지므로, 내부 선언이 중요하다고 언급된다 .
- 비동기 함수가 포함될 경우, 의도와 다르게 실행될 수 있으며, 이로 인해 API 호출순서가 변경되어 사용자가 예상한 결과와 다르게 나타날 수 있다 .
- useEffect의 셋업 함수에는 비동기 함수를 직접 포함하지 말고, 별도로 생성한 후 실행해야 하며, 정확한 흐름의 보장을 위해 항상 주의가 필요하다고 강조된다 .
3.1.3 useMemo
- 비용이 큰 연산에 대한 결과를 저장하고 저장된 값을 반환하는 훅이다.
- 첫 번째 인수로 값을 반환하는 생성 함수를, 두 번째 인수로 해당 함수가 의존하는 값의 배열이 들어간다.
- 의존성 배열의 값이 변경 될 경우 첫 번째 인수의 함수를 실행 후에 그 값을 반환하고 그 값을 기억해준다. 변경되지 않을 경우에는 기억하고 있는 값을 그대로 전달해 준다.
3.1.4 useCallback
- 인수로 넘겨받은 콜백 자체를 기억한다.
- 첫 번째 인수로 함수를, 두 번째 인수로 의존성 배열이 들어간다.
- 렌더링 발생 시 의존성 배열이 변경 됐다면 함수를 재생성하고 그 함수를 기억한다. 변경되지 않았다면 기존 함수를 그대로 반환한다.
3.1.5 useRef
- useState와 마찬가지로 렌더링 후에 변경 가능한 값을 저장할 수 있다.
- useRef는 값이 변하더라도 렌더링을 발생시키지 않는다.
- useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경이 가능하다.
- 가장 일반적인 사용 예는 DOM에 접근하고 싶을 때이다. useRef의 최초 기본 값은 return문에 정의해둔 DOM이 아니고 useRef()로 넘겨받은 인수이다. 따라서 useRef가 선언된 당시에는 컴포넌트가 존재하지 않으므로 undefined가 된다.
3.1.6 useContext
- props drilling을 극복하기 위해 등장했다.
- <Provider>로 값을 가져오고, 여러개의 Provider가 있으면 가장 가까운 Provider 값을 가져온다.
- 컴포넌트 내부에서 사용하면 컴포넌트의 재활용성이 떨어진다.
- Context는 전역상태관리용이 아닌 단방향으로 상태를 주입하는 기능을 한다. 하지만 memo와 함께 사용하면 최적화를 개선할 수 있다.
3.1.7 useReducer
- 첫번째 인수는 값을 업데이트하는 함수이거나 값 그 자체여야 한다. 두번째 인수는 초깃값이다. 세번째 값은 이 두번째 값을 기반으로 한 게으른 초기화를 하는 함수다.
- useState처럼 클로저를 활용해 값을 가둬서 state를 관리하는 공통점이 있다.
function reducer(prevState, newState) {
return typeof newState === 'function' ? newState(prevState) : newState
}
function init(initialArg: initializer) {
return typeof initialArg === 'function' ? initialArg() : initialArg
}
// useState의 작동을 흉내낼 수 있음
function useState(initialArg) {
return useReducer(reducer, initialArg, init)
}
3.1.8 useImperativeHandle
forwardRef 살펴보기
- ref를 전달하는 데 있어 안정적이고 일관성 있게 제공할 수 있게 해준다.
const ChildComponent = forwardRef((props, ref) => {
useEffect(()=>{
// {current: undefined}
// {current: HTMLInputElement}
console.log(ref)
},[ref])
return <div>안녕!</div>
})
function ParentComponent() {
const inputRef = useRef()
return (
<>
<input ref={inputRef} />
<ChildComponent ref={inputRef} />
</>
)
}
useImpretiveHandle 이란?
- 부모에게서 넘겨받은 ref를 원하는대로 수정할 수 있는 훅이다.
import React, { useImperativeHandle, forwardRef, useRef } from 'react';
// 자식 컴포넌트
const ChildComponent = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
});
// 부모 컴포넌트
function ParentComponent() {
const childRef = useRef();
const handleClick = () => {
childRef.current.focus();
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>Focus Child Input</button>
</div>
);
}
export default ParentComponent;
//출처[co-yong 님의 블로그:티스토리]
3.1.9 useLayoutEffect
- useEffect와 사용하는 방법은 같지만 모든 DOM의 변경 후에 useLayoutEffect의 콜백함수 실행이 동기적으로 발생한다는 점이 특징이다.
- 다음은 실행순서이다.
리액트가 DOM을 업데이트 |
useLayoutEffect를 실행 |
브라우저에 변경 사항을 반영 |
useEffect를 실행 |
- 순서상으로는 useEffect가 먼저 선언돼도 useLayoutEffect가 먼저 실행된다.
- 위에 동기적으로 발생한다는 것은 리액트가 useLayoutEffect의 실행이 종료될 때까지 기다린 다음에 화면을 그린다는 의미다. 이로 인해 성능에 문제가 발생할 수 있다.
- DOM은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을때처럼 꼭 필요할때만 사용하는 것이 좋다.
3.1.10 useDebugValue
사용자 정의 훅 내부의 내용에 대한 정보를 남길 수 있는 훅이다.
두번째 인수로 포매팅 함수를 전달하면 이에 대한 값이 변경됐을 때만 호출되어 포매팅된 값을 노출한다. 즉, 첫번째 인수의 값이 같으면 포매팅 함수는 호출되지 않는다.
import React, { useState, useDebugValue } from 'react';
// 커스텀 훅 예시
function useToggle(initialState: boolean) {
const [state, setState] = useState(initialState);
// useDebugValue 사용
useDebugValue(state ? "On" : "Off");
const toggle = () => setState(prevState => !prevState);
return { state, toggle };
}
const ToggleComponent: React.FC = () => {
const { state, toggle } = useToggle(false);
return (
<div>
<p>The state is {state ? "On" : "Off"}</p>
<button onClick={toggle}>Toggle</button>
</div>
);
};
export default ToggleComponent;
- 위 예제에서 useToggle 커스텀 훅은 불리언 값을 토글하는 기능을 한다.
- useDebugValue는 state 값이 true일 때 "On", false일 때 "Off"라고 표시하여 React 개발자 도구에서 쉽게 확인할 수 있다.
3.1.11 훅의 규칙
- 최상위에서만 훅을 호출해야 한다.
- 반복문이나 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다.
- 훅을 호출할 수 있는 것은 리액트 함수 컴포넌트, 사용자 정의 훅 두가지 뿐이다.
- 일반 자바스크립트 함수에서는 훅을 사용할 수 없다.
3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
효율성을 높이고 유지보수를 쉽게 하기 위해 반복되는 작업은 재사용할 수 있게 관리하는 것이 좋다. 이를 위해 사용자 정의 훅(커스텀 훅)과 고차 컴포넌트 이렇게 두가지 방법이 있다.
3.2.1 사용자 정의 훅
- 리액트에서만 사용할 수 있는 훅으로 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용된다.
- 이름이 반드시 use로 시작하는 함수여야 한다.
3.2.2 고차 컴포넌트
- 컴포넌트 자체의 로직을 재사용하기 위한 방법이다.
- 리액트에서 가장 유명한 고차 컴포넌트는 React.memo다.
React.memo
- props의 변화가 없음에도 컴포넌트의 렌더링을 방지하기 위해 만들어졌다.
- 렌더링하기에 앞서 props를 비교해 이전과 props가 같다면 렌더링 자체를 생략하고 이전에 기억해 둔 컴포넌트를 반환한다.
- useMemo는 값을 반환하기 때문에 JSX 함수 방식이 아닌 {}을 사용한 할당식을 사용한다는 점에서 차이가 있다.
고차 함수를 활용한 리액트 고차 컴포넌트 만들어보기
- 고차 함수의 정의는 "함수를 인수로 받거나 결과로 반환하는 함수"이다.
- 앞에 with로 시작해야 한다.
- 사용 시 부수 효과를 최소화해야 한다.
- 최소한으로 사용하는 것이 좋다.
- 아래는 예제와 그 설명이다.
import React, { useState } from 'react';
// HOC 정의
function withCounter(Component: React.ComponentType<any>) {
return function WithCounter(props: any) {
const [count, setCount] = useState(0);
const increment = () => setCount(prevCount => prevCount + 1);
// 원래 컴포넌트에 카운터 값과 증가 함수 전달
return <Component {...props} count={count} increment={increment} />;
};
}
// 카운터를 표시하는 컴포넌트
function CounterDisplay({ count, increment }: { count: number; increment: () => void }) {
return (
<div>
<p>Current count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
// HOC를 사용해 CounterDisplay에 카운터 기능 추가
const EnhancedCounterDisplay = withCounter(CounterDisplay);
const App: React.FC = () => {
return (
<div>
<h1>Higher-Order Component Example</h1>
<EnhancedCounterDisplay />
</div>
);
};
export default App;
- withCounter: withCounter는 고차 함수로, Component를 인수로 받아서 카운터 기능을 추가한 새로운 컴포넌트를 반환한다. 이 컴포넌트는 카운트 값과 카운트를 증가시키는 increment 함수를 prop으로 전달한다.
- CounterDisplay: 카운트를 표시하고 버튼을 눌러 값을 증가시킬 수 있는 간단한 컴포넌트이다.
- EnhancedCounterDisplay: withCounter HOC를 사용하여 CounterDisplay에 카운터 기능을 추가한 컴포넌트이다.
- App: 최종적으로 EnhancedCounterDisplay를 렌더링하여, 카운터 기능이 포함된 컴포넌트를 화면에 표시한다.
3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
- 단순히 useEffect, useState와 같이 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있다면 자체적으로 렌더링에 영향을 주지 않는 사용자 정의 훅을 사용하는 것이 좋다.
- 반대로 렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하는 것이 좋다.
'React-study > dil' 카테고리의 다른 글
[모던 리액트 Deep Dive] 6장 리액트 개발 도구로 디버깅하기 (0) | 2024.11.18 |
---|---|
[모던리액트 Deep Dive] 5장 리액트와 상태 관리 라이브러리 (4) | 2024.11.14 |
[모던 리액트 Deep Dive] 2장 리액트 핵심 요소 깊게 살펴보기 (12) | 2024.11.06 |
[모던리액트 Deep Dive] 1장 리액트 개발을 위해 꼭 알아야 할 자바스크립트 (7) | 2024.11.05 |
[DIL] useDeferredValue (0) | 2024.06.24 |