리액트 훅 깊게 살펴보기 (2)
모던 리액트 Deep Dive: 리액트의 핵심 개념과 동작 원리부터 Next.js까지, 리액트의 모든 것 - 03장 리액트 훅 깊게 살펴보기 (2)
1. 리액트의 모든 훅 파헤하기
useRef
useRef
는 useState
와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점이 있다. 그러나 useState
와 구별되는 큰 차이점 두 가지를 가지고 있다.
useRef
는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.useRef
는 그 값이 변하더라도 헨더링을 발생시키지 않는다.
useRef
는 컴포넌트가 렌더링될 때만 생성되며, 컴포넌트 인스턴스가 여러 개라도 각각 별개의 값을 바라본다.
useRef
의 사용 예는 DOM에 접근하고 싶을 때이다.
function RefComponent() {
const inputRef = useRef()
console.log(inputRef.current) // undefined
useEffect(() => {
console.log(inputRef.current) // <input type="text"></input>
}, [inputRef])
return <input ref={inputRef} type="text" />
}
useRef
는 최초에 넘겨받은 기본값을 가지고 있으며 최조 기본값은 useRef()
로 넘겨받은 인수다. useRef
가 선언된 당시에는 아직 컴포넌트가 렌더링되기 전이라 return
으로 컴포넌트의 DOM이 반환되기 전이므로 undefined
다.
useRef
를 사용할 수 있는 유용한 걍우는 렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있다.
개발자가 원하는 시점의 값을 렌더링에 영향을 미치지 않고 보관해 두고 싶다면 useRef
를 사용하는 것이 좋다.
useContext
Context란?
<A props={something}>
<B props={something}>
<C props={something}>
<D props={something}/>
</C>
</B>
</A>
A 컴포넌트에서 제공하는 데이터를 D 컴포넌트에서 사용하려면 props
를 하위 컴포넌트로 필요한 위치까지 계속해서 넘겨야 한다. 이러한 기법을 prop
내려주기(prop drilling)라고 한다.
prop
내려주기를 극복하기 위해 등장한 개념이 바로 콘텍스트(Context)다. 콘텍스트를 사용하면, 이러한 props 전달 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있다.
Context를 해당 컴포넌트에서 사용할 수 있게 해주는 useContext 훅
const Context = createContext<{ hello: string } | undefined>()
function ParenComponent() {
return (
<>
<Context.Provider value=>
<Context.Provider value=>
<ChildComponent />
</Context.Provider>
</Context.Provider>
</>
)
}
function ChildComponent() {
const value = useContext(Context)
return <>{value ? value.hello : ''}</>
}
useContext
는 상위 컴포넌트에서 만들어진 Context를 함수형 컴포넌트에서 사용할 수 있도록 만들어진 훅이다. useContext
를 사용하면 상위 컴포넌트 어딘가에서 선언된 <Context.Provider />
에서 제공한 값을 사용할 있게 된다. 여러 개의 Provider
가 있다면 가장 가까운 Provider
의 값을 가져오게 된다.
useContext를 사용할 때 주의할 점
useContext
를 함수형 컴포넌트 내부에서 사용할 때는 항상 컴포넌트 재활용이 어려워진다.
useContext
가 선언돼 있으면 Provider
에 의존성을 가지고 있어 재활용하기에는 어려운 컴포넌트가 된다.
이러한 상황을 방지하려면 useContext
를 사용하는 컴포넌트를 최대한 작게 하거나 혹은 재사용되지 않을 만한 컴포넌트에서 사용해야 한다.
상태 관리 라이브러리가 되기 위해서는 최고한 다음 두 가지 조건을 만족해야 한다.
- 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
- 필요에 따라 어떠한 상태 변화를 최적화할 수 있어야 한다.
useContext
는 상태를 관리하는 것이 아니다.
useReducer
useState
보다 복잡한 상태 값을 미리 정의해 관리할 수 있다.
useReducer
에서 사용되는 용어이다.
- 반환값은
useState
와 동일하게길이가 2인 배열이다.state
: 현재useReducer
가 가지고 있는 값을 의미힌다. 배열을 반환하는데, 첫 번째 요소가 값이다.dispatcher
:state
를 업데이트하는 함수.useReducer
가 반환하는 배열의 두 번재 요소다.action
을 넘겨준다. 이action
은state
를 변경할 수 있는 액션을 의미한다.
useState
의 인수와 달리 2개에서 3개의 인수를 필요로 하다.reducer
:useReducer
의 기본action
을 정의하는 함수다.useReducer
의 첫 번째 인수로 넘겨주어야 한다.initalState
: 두 번째 인수로,useReducer
의 초깃값을 의미한다.init
:useState
의 인수로 함수로 넘겨줄 때처럼 초깃값을 지연해서 생성시키고 싶을 때 사용하는 함수다.(생략가능)
type State = {
count: number
}
type Action = { type: 'up' | 'down' | 'reset'; payload?: State }
function init(count: State):State {
return count
}
const initialState: State = { count: 0 }
function reducer(state: State, action: Action): State {
switch (action.type) {
case: 'up':
return { count: state.count + 1 }
case: 'down':
return { count: state.count - 1 > 0 ? state.count - 1 : 0 }
case: 'reset':
return init(action.payload || { count: 0 })
default:
throw new Error(`Unexpected action type ${action.type}`)
}
}
export defatul function App() {
const [state, dispatcher] = useReducer(reducer, initialState, init)
function handleUpButtonClick() {
dispatcher({ type: 'up' })
}
function handleDownButtonClick() {
dispatcher({ type: 'down' })
}
function handleResetButtonClick() {
dispatcher({ type: 'reset', payload: { count: 1 } })
}
return (
<div className="App">
<h1>{state.count}</h1>
<button onClick={handleUpButtonClick}>+</button>
<button onClick={handleDownButtonClick}>-</button>
<button onClick={handleResetButtonClick}>reset</button>
</div>
)
}
복잡한 형태의 state
를 사전에 정의된 dispatcher
로만 수정할 수 있게 만들어 줌으로써 state
값에 대한 접근은 컴포넌트에서만 가능하게 하고, 이를 업데이트하는 방법에 대한 상세 정의는 컴포넌트 밖에다 둔 다음, state
의 업데이트를 미리 정의해 둔 dispatcher
로만 제한하는 것이다. state
값을 변경하는 시나리오를 제한적으로 두고 이에 대한 변경을 빠르게 확인할 수 있게끔 하는 것이 useReducer
의 목적이다.
세 번째 인수의 게으른 초기화 함수는 생략 가능하다. 없다면 두 번째 인수로 넘겨받은 기본값을 사용한다. 게이른 초기화 함수를 넣어줌으로써 useState
에 함수를 넣은 것과 같은 동일한 이점이 있고 추가로 state
에 대한 초기화가 필요할 때 reducer
에서 이를 재사용할 수 있다는 장점도 있다.
useReducer
는 클로저를 활용해 값을 가둬서 state
를 관리한다.
useImperativeHandle
forwardRef 살펴보기
ref
는 useRef
에서 반환한 객체로, 리액트 컴포넌트의 props
인 ref
에 넣어 HTMLElement에 접근하는 용도로 흔히 사용된다.
function ChildComponent({ref}) {
useEffect(() => {
console.log(ref)
}, [ref])
return <div>안녕!</div>
}
function ParentComponent() {
const inputRef = useRef()
return (
<>
<input ref={inputRef} /> // 오류
</>
)
}
리액트에서 ref
는 props
로 쓸 수 없다는 경고문과 함께 접근을 시도할 경우 undefined를 반환한다.
forwardRef
는 ref
를 전달하는 데 있어서 일관성을 제공하기 위해서다. 어떤 props
명으로 전달할지 모르고, forwardRef
를 사용하여 좀 더 확실하게 ref
를 전달할 것임을 예측할 수 있다.
const ChildComponent = forwardRef((props, ref) => {
useEffect(() => {
console.log(ref)
}, [ref])
return <div>안녕!</div>
})
function ParentComponent() {
const inputRef = useRef()
return (
<>
<input ref={inputRef} />
<ChildComponent ref={inputRef} />
</>
)
}
ref
를 받고자 하는 컴포넌트를 forwardRef
로 깜싸고, 두 번째 인수로 ref
를 전달받는다. 그리고 부모 컴포넌트에서는 동일하게 props.ref
를 통해 ref
를 넘겨주면 된다.
useImperativeHandle이란?
useImperativeHandle
은 부모에게서 넘겨받은 ref
를 원하는 대로 수정할 수 있는 훅이다.
const Input = forwardRef((props, ref) => {
useImperativeHandle(
ref,
() => ({
alert: () => alert(props.value),
}),
[props.value],
)
return <input ref={ref} {...props} />
})
function App() {
const inputRef = useRef()
const [text, setText] = useState('')
function handleClick() {
inputRef.current.alert()
}
function handleChange(e) {
setText(e.target.value)
}
return (
<>
<input ref={inputRef} value={text} onChnage={handleCnage} />
<button onClick={handleClick}>Focus</button>
</>
)
}
useImperativeHandle
을 사용하면 부모 컴포넌트에서 노출되는 값을 원하는 대로 바꿀 수 있다.
useImperativeHandle
을 사용하면 이 ref
의 원하는 값이나 액션을 정의할 수 있다.