Hooks
기본 Hook
useState
1 | const [state, setState] = useState(initialState); |
useState():state와setState을 반환하는 함수initialState: 초기값- 보통
null이나""등의 default value로 설정한다.
- 보통
state: 상태 유지 (저장) 값- 최초 렌더링 시 반환된
state는initialValue이다.
- 최초 렌더링 시 반환된
setState:state값을 갱신하는 함수1
setState(newState);
- 새
state값을 받으면 컴포넌트 리렌더링을 큐에 등록한다. - 리렌더링 시
useState가 반환하는 첫번째 값 (인자) 은 갱신된 최신state이다.
- 새
Note
React guarantees that
setStatefunction identity is stable and won’t change on re-renders. This is why it’s safe to omit from theuseEffectoruseCallbackdependency list.
Functional Updates
함수적 갱신
setState가 값을 갱신하는 과정을 직접 코드로 작성하면 다음과 같다.1
2
3
4
5const [state, setState] = useState({});
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});갱신된 객체를 자동으로 merge하는
useState와 반대로,setState는initialValue를 유지한다.이전
state를 이용해서 값을 갱신하는 경우, 이전 값을 받아 갱신된 값을 반환하는 함수를setState로 전달하면 된다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function Counter ({ initialCount }) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
// 초기화
<button onClick={() => setCount(initialCount)}>Reset</button>
// 이전 값을 이용해서 값을 갱신하는 경우
// parameter value : 현재 값
// return value : 갱신할 값
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
Note
Another option is
useReducer, which is more suited for managing state objects that contain multiple sub-values.
- 업데이트 함수 (setState에 전달한 함수) 가 현재 상태와 정확히 동일한 값을 반환한다면 바로 뒤에 일어날 리렌더링은 완전히 건너뛰게 된다.
React에서
{}를 쓰는 경우
- JSX 내부에서는 객체를 받아옴
{{}}: 객체를 받아오는 것이 아니라 JSX 내부에서 생성할 때- JSX 외부에서는 일반 JS의 문법 중 function destructurizing을 실행
Lazy Initial State
지연 초기 state
initialState 인자는 초기 렌더링 시에 사용하는 state (값) 으로, 리렌더링 시 이 값은 무시된다.
만약 초기 state가 고비용 계산의 결과라면, 초기 렌더링 시에만 실행될 함수를 정의하여 전달하는 것이 효율적이다.
1 | const [state, setState] = useState(() => { |
Bailing Out of a State Update
state 갱신의 취소
앞서 설명한 “업데이트 함수 (setState에 전달한 함수) 가 현재 상태와 정확히 동일한 값을 반환한다면 바로 뒤에 일어날 리렌더링은 완전히 건너뛰게 된다.” 를 추가적으로 설명하자면,
State Hook을 현재 state와 동일한 값으로 갱신 (update) 할 경우, 리액트는 children or firing effects를 렌더링하는 과정을 건너뛰고 실행 (처리) 을 종료한다.
Note
React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with
useMemo.
useEffect
함수형 컴포넌트 내에서는 React’s render phase에 따라 Mutations, subscriptions, timers, logging, and other side effects 가 허용되지 않는다.
이를 위해 useEffect 를 사용할 수 있다.
1 | useEffect(didUpdate); |
useEffect(): 모든 렌더링이 완료된 후 / 어떤 값이 변경되었을 때에만 동작 (didUpdate) 을 수행didUpdate: 특정 effect를 발생시키는 (명령형) 함수
Cleaning Up an Effect
effect 정리
Effect는 컴포넌트가 화면에서 제거될 때 정리되어야 하는 요소 (e.g. subscription, timer ID 등) 를 만들곤 한다. 이를 정리하는 함수를 useEffect 에 전달할 수 있다.
1 | useEffect(() => { |
clean-up function(e.g.subscription.unsubscribe())은 컴포넌트가 제거되기(=== 다음의 effect) 이전 에 실행되어 memory leaks를 방지한다.- 컴포넌트가 여러번 리렌더링 될 경우, 이전의 effect는 다음의 effect가 실행되기 이전 에 정리된다.
Timing of Effects
effect 타이밍
useEffect에 전달된 함수의 경우 레이아웃 배치과 그리기를 완료한 후, 지연된 이벤트가 발생하는 동안 실행된다. 이는 브라우저가 화면을 업데이트하는 과정을 방해하지 않기 때문에, 여러 side effects (e.g. setting up subscriptions and event handlers) 를 실행하기에 적합한 때이다.
useEffect 는 브라우저가 그려지기 이전까지는 대기하지만, 새로운 렌더링이 발생하기 이전에 실행되는 것을 보장한다. 리액트는 언제나 새로운 업데이트가 발생하기 이전에 이전의 렌더링 effect를 모두 제거한다.
useLayoutEffectDOM 변경과 같이 사용자에게 effect가 보여지는 경우, visual inconsistency 방지를 위해 다음 execution (event) 이 실행되기 이전에 현재 화면이 렌더링이 됨과 동시에 effect가 (synchronously) 발생하여야 한다.
Conditionally Firing an Effect
조건부 effect 발생
Effect의 default behavior은 렌더링이 모두 완료된 이후 실행되는 것이다. 이는 해당 effect의 dependency 중 하나가 변경될 때마다 새로 실행됨을 의미한다.
앞선 예제를 통해서 자세히 보면,
1 | useEffect(() => { |
subscription 은 매 업데이트마다 생성될 필요 없이, source prop 이 변할 때만 새로 생성되면 된다.
이와 같은 현상을 방지하기 위해, useEffect 의 2번째 argument로 effect가 의존하는 (depend on, dependency) 배열을 전달한다.
따라서 위 예제를 수정해보면,
1 | useEffect( |
이를 통해 subscription은 props.source가 변경될 때에만 재생성된다.
만약 effect를 (mount 또는 unmount 시) 한 번만 수행하고 싶다면 두번째 인자로 빈 배열 ( [] ) 을 전달하면 된다. 이를 통해 effect는 컴포넌트 범위에서 가져온 값들 (e.g. props, state 등) 에 전혀 의존하지 않으므로 해당 값들이 변경되어도 다시 실행되지 않는다.
빈 배열 ( [] ) 은 effect 안에 있는 props 와 state 가 항상 초기값을 가지게 됨을 의미한다. 이는 componentDidMount 와 componentWillUnMount 의 기능과 같다.
두번째 인자로 전달하는 배열은 effect에 사용되는 컴포넌트 범위의 모든 값 (e.g. props, state 등) 들을 포함해야 한다. 이를 위반할 시 이전 렌더링에서 설정한 값을 참조하는 버그가 발생한다.
이렇게 전달된 dependency 배열은 effect 함수에 인자로 전달되지 않는다. 하지만 effect 함수 내에서 참조된 모든 값들이 해당 배열에 포함되어야 하므로, 전달 여부는 유의미하지 않다.
useContext
1 | const value = useContext(MyContext); |
useContext(): (React.createContext를 통해 반환된 값인 ) context 객체에 **현재 context 값 (MyContext) **을 반환한다.- 가장 가까운 상위 컴포넌트
<MyContext.Provider>가 업데이트 되면,useContext는 해당 컴포넌트에 전달 (포함) 된 가장 최신의 context value 에 따라 화면을 리렌더링 한다.
- 가장 가까운 상위 컴포넌트
MyContext: 현재 context 값 (객체) 으로, 트리 구조에서 해당 컴포넌트의 상위 컴포넌트이면서 가장 가까운<MyContext.Provider>의value prop에 따라 결정된다.useContext()의 인자는 context 객체여야 한다.- Correct:
useContext(MyContext) - Incorrect:
useContext(MyContext.Consumer) - Incorrect:
useContext(MyContext.Provider)
- Correct:
useContext 를 사용한 컴포넌트는 context 값이 변경될 때마다 리렌더링된다. 리렌더링 되는 컴포넌트가 비싸다면, momoization 을 이용해 효율성을 높일 수 있다.
상위 컴포넌트에서
React.memo나shouldComponentUpdate를 쓰더라도,useContext를 사용한 컴포넌트부터 리렌더링이 진행 (시작) 된다.
useContext 는 context를 읽고 변경사항을 구독 (확인) 하는 것만 가능하기 때문에, 해당 context의 값을 전달하기 위해 트리 구조 내 상위 컴포넌트로서 <MyContext.Provider> 가 필요하다.
Putting it together with Context.Provider
1 | const themes = { |
추가 Hooks
useReducer
useState() 의 대체재로서 사용한다. 여러 하위 값을 포함하거나 다음 state가 이전의 state 값에 영향을 받는 복잡한 state 로직을 가진 경우 선호된다.
컴포넌트의 state 업데이트 로직을 컴포넌트에서 분리하여 컴포넌트 바깥에서 작성하거나 다른 파일에서 작성한 후 불러와서 사용할 수도 있다.
또한 callback 대신 dispatch를 전달한 수 있어 deep update를 요구하는 컴포넌트의 경우 performance를 최적화할 수 있다.
What is deep update in React?
먼저, React에서 update란;
useState()를 사용하는 모든 리액트 컴포넌트에서 사용가능한 함수로, 컴포넌트 state가 변경되었을 때 리액트에게 이를 알림으로써 해당 state에 종속된 UI를 업데이트하기 위해 컴포넌트에게 리렌더링이 필요하다는 것을 전달한다.따라서, deep update란 deep nested 객체 (many pieces of information과 fixed 스키마를 가진) 를 update 하는 것을 의미한다.
1 | const [state, dispatch] = useReducer(reducer, initialArg, init); |
useReducer():reducer를 받아 현재state와 짝지어진dispatch함수를 반환한다.reducer: 현재state와action객체를 파라미터로 받아와서 ((state, action)) 새로운 state를 반환하는 함수(state, action) => newState의 구조를 가진다.1
2
3
4
5function reducer(state, action) {
// 새로운 state를 만드는 로직
// const nextState = ...
return nextState;
}반환하는 해당 state는 컴포넌트가 지닐 새로운 state가 된다.
action: 업데이트를 위한 정보를 가지고 있다.주로
type값을 지닌 객체 형태로 사용한다.type의 경우, 대문자 를 사용하거나_로 시작하는 관습이 있다.
state: 컴포넌트에서 사용할 수 있는 상태dispatch:action을 (인자로 받아) 발생시키는 함수1
dispatch({ type: 'INCREMENT' })
1 | const initialState = {count: 0}; |
Note
리액트는 리렌더링이 되더라도 dispatch 함수의 identity 는 동일하게 유지하기 때문에, useEffect 나 useCallback 의 dependency list에 포함하지 않는다.
Specifying the Initial State
초기 state의 구체화
useReducer() 의 state를 초기화하는 방법에는 2가지가 있다.
- 가장 간단한 방법으로는, initial state를 2번째 인자로 전달하는 방법이 있다.
1 | const [state, dispatch] = useReducer( |
Lazy Initialization
초기화 지연
- initial state를 lazily 생성하는 방법으로, init 함수를 3번째 인자로 전달하여 initial state를 init(initialArg) 로 설정할 수 있다.
- initial state 를 계산하는 로직을 reducer 밖에 정의하여 action 에 따른 state 재설정 시 유용하게 사용할 수 있다.
1 | // initialization 함수를 외부에 정의 |
payload전송되는 데이터
Bailing Out of a Dispatch
dispatch의 회피
앞서 useState() 에서 설명한 것과 같이, Reducer Hook ( useReducer() ) 이 현재 state과 동일한 값을 반환할 경우, 자식을 렌더링하거나 effect를 실행하지 않고 처리 (i.e. 렌더링, 이벤트) 를 종료한다.
Object.is()comparison 알고리즘을 이용
처리를 종료하기 전에 해당 컴포넌트를 다시 렌더링해야 하는 경우가 존재하지만, 그 이상으로 깊게 트리를 탐색하지 않으므로 문제가 되지는 않는다. 만약 렌더링에 비싼 계산 과정 이 포함된다면, useMemo() 를 사용할 수 있다.
useCallback
memoized callback 을 반환한다.
memoization
비싼 함수의 호출 결과를 저장하고 같은 input이 발생할 경우 저장된 (캐시) 값을 반환하여 프로그램의 수행 속도를 증가시키는 최적화 (optimization) 기술
(some sort of) 캐시를 이용하여 (비싼) 함수를 부르는 횟수를 제한하는 것
- 특정 input에 대한 캐시값이 없다면, original 함수가 한번 호출되어 (캐시) map에 결과값이 추가된다.
- 캐시를 지우지 않는 한 결과는 캐시 map에서 반환된다.
👉
useCallbackhook 은 다른 기능을 한다.
🌈 REFERENCE를 꼭 읽어보세염 (:->
useCallback() 을 이용하는 이유는, callback 함수인 cb의 memoized 버전을 받기 위해서이다.
컴포넌트가 처음으로 렌더링될 때, 새로운 cb 가 생성되고 똑같은 cb 함수가 useCallback() 에 의해 반환된다.
위 예시에서 memoizedCb() 가 호출되면, original cb 콜백함수가 호출되어 결과적으로 original cb 함수를 호출한다.
컴포넌트가 리렌더링 (2번째) 되면, 새로운 cb 가 생성된다. 하지만 useCallback() 에 전달된 dependency가 변하지 않았기 때문에, 생성된 cb는 버려지고 memoizedCb는 첫번째 렌더링 때 생성된 cb를 유지한다. 결국 이 때의 memoizedCb 호출은 이전과 같이 기존의 (old) cb 를 호출하여 original cb 함수를 호출하게 된다.
dependency가 변하지 않는 이상, 모든 렌더링은 위와 같은 과정을 반복하여 사용하지 않는 콜백함수를 생성하고 기존의, memoized 콜백을 호출하여 결국 original cb 함수를 호출한다.
dependency 가 변한 경우, memoizedCb 도 변한다.
이전의 memoizedCb 가 첫번째 cb를 호출하는 과정과 비슷하게, 이 때의 memoizedCb 또한 dependency 가 변한 시점부터 첫번째 cb 를 호출한다.
callback특정 함수 (콜백함수) 를 다른 함수에 전달하고 이를 호출하는 특정 이벤트 (e.g. a query finished or an error occurs) 가 발생하면 콜백함수를 발생시키는 것
- 여러 조각의 코드의 소통을 정의하여, 콜백함수를 호출하는 모듈이 결과를 사용하는 것이 아니라 이벤트가 발생했을 때 호출을 받는 함수에 콜백함수를 전달한다.
- 여기서 중요한 점은, one piece of code가 콜백함수를 호출하면 다른 (조각의) 코드가 이 호출을 받는다는 것이다. 여기서 어떠한 호출 코드도 잃으면 (생략되면) 안 되기 때문에, 콜백함수를 memoizing하여 모듈 간 소통 line을 끊는 방법이 탄생한 것이다.
결과적으로 useCallback() 이 하는 것은 똑같은 callback 의 횟수를 제한하는 것이다. 결과값들은 캐시에서 반환되지 않고, dependency 가 변한 시점으로부터 첫번째의 cb 와 같은 memoizedCb 를 반환한다.
1 | const memoizedCallback = useCallback( |
useCallback(): inline callback 함수와 dependencies 배열을 전달- memoized version of the callback을 반환하며, dependencies가 변했을 때만 memoized version 도 변경된다.
useCallback(fn, deps)===useMemo(() => fn, deps)
불필요한 렌더링을 방지하기 위해 reference의 동일성에 의존하는 (동일성에만 영향을 받는) (최적화된) 자식 컴포넌트에 콜백함수를 전달할 때 유용하다.
앞선
useEffect와 마찬가지로, dependencies 배열은 콜백함수에 인자로 전달되지 않는다.하지만 정의 자체가, “콜백함수에서 참조하는 모든 값들은 dependencies 배열에 포함되어야 한다” 이다.
useMemo
memoized 값을 반환한다.
useMemovs.useCallback
useCallback(): 함수 반환
useMemo(): 값 반환
1 | const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); |
useMemo():create 함수와dependencies 배열을 전달- dependency 가 변할 때에만 memoized 값을 다시 계산한다. 이를 통해 비싼 계산 과정이 매 렌더링마다 발생하는 것을 방지한다.
- 2번째 인자 (
dependencies 배열로서) 에 빈 배열 ([]) 이 전달되면, 매 렌더링 마다 새로운 값이 계산된다.
useMemo() 에 전달된 create 함수 는 렌더링 동안 실행된다.
보통 side effect는
useMemo가 아니라useEffect에서 발생한다. 따라서 렌더링 동안은 다른 행동을 하지 않아야 한다. (당연함 -.-)
❗️
useMemo()는 성능 최적화에만 사용하고, 큰 의미적 기능 (semantic guarantee) 을 담지 않아야 한다. 따라서useMemo()를 사용하지 않고도 동작할 수 있도록 코드를 작성하고, 성능 최적화를 위해useMemo()를 추가하도록 한다.
앞선
useEffect,useCallback과 마찬가지로,depedencies 배열은 콜백함수에 인자로 전달되지 않는다.
useRef
JavaScript를 이용해 특정 DOM 을 선택해야 하는 경우 getElementById, querySelector 와 같은 DOM Selector 함수 를 사용한다.
리액트를 사용할 때도 특정 DOM 을 선택해야 하는 상황이 발생하는데, 이 때 ref 를 사용한다.
- 함수형 컴포넌트에서
ref를 사용 할 때에는useRef라는 Hook 함수 를 사용한다. - 클래스형 컴포넌트에서는 콜백 함수를 사용하거나
React.createRef라는 함수를 사용한다.
1 | const refContainer = useRef(initialValue); |
useRef(): (생성된 ref 객체의).current프로퍼티 (속성) 가 전달된 인자 (e.g.initialValue) 로 초기화된, 변경 가능한 ref 객체를 반환한다.- 변경가능한 값을 넣을 수 있는
.current 프로퍼티를 갖고 있는 “박스” 와 같다. - 해당 객체는 컴포넌트의 full lifetime 동안 지속된다.
- 변경가능한 값을 넣을 수 있는
useRef() 는 DOM에 접근하는 방법으로 많이 사용된다.
useRef() 를 사용하여
- Ref 객체를 만들고,
- 이 객체를 우리가 선택하고 싶은 DOM (노드) 에
ref값으로 설정한다. - 그러면, Ref 객체의
.current값은 우리가 원하는 DOM 을 가르키게 된다.
1 | <div ref={refContainer} /> |
refContainer 의 .current 프로퍼티를 해당 ref 객체 ( refContainer ) 를 ref 로 설정한 DOM 노드로 설정하고, 노드가 변경될 때마다 그에 맞게 .current 프로퍼티를 조정한다.
하지만
useRef()는.current에 변경 사항이 존재하더라도 리렌더링 하지 않기 때문에, 이에 대해 notify 하지 않는다. 만약 리액트가 DOM 노드에 ref 객체를 연결하거나 제거할 때 notify 하고 싶다면,callback ref를 이용해야 한다.
1 | function TextInput_With_FocusButton() { |
HTMLElement.focus()특정 element 에 (focus를 맞출 수 있을 때) focus를 맞추는 함수로, 해당 element는 키보드 등의 이벤트를 받는 default element로 정의된다.
useRef() 는 ref 속성 외에도, 클래스가 instance field (인스턴스 변수) 를 가지고 있는 것처럼 ref 객체에 변경 가능한 어떠한 값도 담을 수 있다는 장점이 있다.
이는 useRef() 가 plain JavaScript 객체 를 생성하기 때문인데, 직접 {current: ...} 로 객체를 생성하는 것과 유일한 차이점은 useRef() 는 매 렌더링마다 똑같은 ref 객체를 제공 (반환) 한다는 것이다.
class field (클래스 변수)vs.instance field (인스턴스 변수)
- 클래스 변수 : 여러 인스턴스 (서로 다른 객체) 간에 공유해야 하는 값을 바인딩
- 인스턴스 변수 : 각 인스턴스 (객체) 마다 가지고 있는 고유한 값
파이썬은 인스턴스를 통해 접근한 이름 (변수) 이 인스턴스의 name_space (name들을 정의한 공간) 에 없을 경우, 그 다음으로 클래스의 name_space에서 찾아본다.
참고로 클래스 변수에 접근할 때에는 클래스 이름을 사용하여 바로 변수 (값) 에 접근할 수 있다.
useImperativeHandle
❗️ref를 사용하는 imperative code 는 되도록이면 피하는 것이 좋다. ):-<
ref를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값 (변수) 을 커스터마이징 한다.
즉, forwarding된 ref를 replace할 수 있는 기능을 제공한다.
1 | useImperativeHandle(ref, createHandle, [deps]) |
useImperativeHandle() :
ref,콜백함수 (createHandle),dependnecies 배열을 전달createHandle(): 현재 컴포넌트의 값을 부모 컴포넌트가 접근할 수 있도록 하는 콜백함수
forwardRef()와 함께 사용되어야 한다.forwardRef()(말 그대로) reference를 전달해주는 기능을 하는 함수
1 | function FancyInput(props, ref) { |
<FancyInput ref={inputRef} /> 를 수행 (렌더링) 하는 부모 컴포넌트는 inputRef.current.focus() 를 호출할 수 있다.
useLayoutEffect
useEffect 와 동일한 기능을 DOM 변경 시 동시에 (synchronously) 실행하는 함수로, DOM 에서 레이아웃을 읽음과 동시에 리렌더링을 해야할 때 사용한다.
useLayoutEffect 내부에 정의된 예정된 업데이트 또한 브라우저가 레이아웃을 그리기 이전에, 읽음과 동시에 발생한다.
화면 업데이트를 차단하지 않아도 되는 경우에는
useEffect를 사용하는 것이 좋다.
클래스 컴포넌트에서 코드를 옮길 때
useLayoutEffect()는componentDidMount나componentDidUpdate와 같은 단계에서 발생한다. 하지만, 먼저useEffect를 시도해보고 에러가 발생할 때useLayoutEffect를 사용하는 것이 좋다.
SSR (서버 사이드 렌더링) 의 경우,
useLayoutEffect나useEffect는 JavaScript 가 모두 다운되기 전까지는 실행되지 않는다. SSR 컴포넌트가useLayoutEffect를 포함한 경우,
- 첫 렌더링 시 해당 컴포넌트가 필요하지 않다면
useEffect로 로직을 옮기고,useLayoutEffect가 실행되기 전까지 HTML이 망가져 보인다면 클라이언트 렌더링이 완료될 때까지 해당 컴포넌트를 보여주는 것을 딜레이시킨다.서버 렌더링 HTML에서 레이아웃 effect를 필요로 하는 컴포넌트를 제외하기 위해서,
showCild && <Child />를 이용하여 해당 컴포넌트를 조건적으로 렌더링을 하고,useEffect(() => { setShowChild(true); }, [])를 이용하여 보이는 것을 미룬다 (defer).이를 통해 HTML이 hydration (
useLayoutEffect,useEffect실행) 이전에 망가져 보이는 것을 방지한다.
useDebugValue
1 | useDebugValue(value) |
React DevTools 에서 커스텀 Hooks의 label (value) 을 보여준다.
1 | function useFriendStatus(friendID) { |
모든 커스텀 Hook에 debug value를 추가하는 것은 좋지 않다.
shared libraries에 속하는 커스텀 Hooks에 유용하게 사용될 수 있다.
Defer Formatting Debug Values
디버그 값 포맷팅 지연하기
디스플레이 (되는) 값을 포매팅하는 것은 고비용 연산일 수 있고, Hook이 감지되지 않은 경우 사실상 포매팅은 불필요하다. 따라서, useDebugValue 의 (optional한) 2번째 인자로 포매팅 함수 를 전달하여 Hook이 감지되었을 때만 해당 함수를 호출하여 포매팅할 수 있다.
1 | useDebugValue(value, value => value.formatting_function()); |
useDebugValue():value와optional formatting 함수를 전달optional formatting function:debug value를 인자로 전달하고formatted display value를 반환한다.
1 | useDebugValue(date, date => date.toDateString()); |
2번째 인자로 fomatting function 을 전달하여 Date value 를 반환하는 커스텀 Hook이 toDateString() 을 불필요하게 호출하는 것을 방지한다.