리액트 훅 깊게 살펴보기 (2)

리액트 훅 깊게 살펴보기 (2)

모던 리액트 Deep Dive: 리액트의 핵심 개념과 동작 원리부터 Next.js까지, 리액트의 모든 것 - 03장 리액트 훅 깊게 살펴보기 (2)

1. 리액트의 모든 훅 파헤하기

useRef

useRefuseState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점이 있다. 그러나 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를 사용하는 컴포넌트를 최대한 작게 하거나 혹은 재사용되지 않을 만한 컴포넌트에서 사용해야 한다.

상태 관리 라이브러리가 되기 위해서는 최고한 다음 두 가지 조건을 만족해야 한다.

  1. 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
  2. 필요에 따라 어떠한 상태 변화를 최적화할 수 있어야 한다.

useContext는 상태를 관리하는 것이 아니다.

useReducer

useState보다 복잡한 상태 값을 미리 정의해 관리할 수 있다.

useReducer에서 사용되는 용어이다.

  • 반환값은 useState와 동일하게길이가 2인 배열이다.
    • state: 현재 useReducer가 가지고 있는 값을 의미힌다. 배열을 반환하는데, 첫 번째 요소가 값이다.
    • dispatcher: state를 업데이트하는 함수. useReducer가 반환하는 배열의 두 번재 요소다. action을 넘겨준다. 이 actionstate를 변경할 수 있는 액션을 의미한다.
  • 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 살펴보기

refuseRef에서 반환한 객체로, 리액트 컴포넌트의 propsref에 넣어 HTMLElement에 접근하는 용도로 흔히 사용된다.

function ChildComponent({ref}) {
	useEffect(() => {
	console.log(ref)
	}, [ref])
	
	return <div>안녕!</div>
}

function ParentComponent() {
	const inputRef = useRef()
	
	return (
	<>
		<input ref={inputRef} /> // 오류
	</>
	)
}

리액트에서 refprops로 쓸 수 없다는 경고문과 함께 접근을 시도할 경우 undefined를 반환한다.

forwardRefref를 전달하는 데 있어서 일관성을 제공하기 위해서다. 어떤 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의 원하는 값이나 액션을 정의할 수 있다.


© 2021. All rights reserved.

Powered by Hydejack v9.1.6