훅 활용시 클래스 컴포넌트가 아니더라도 리액트의 다양한 기능을 활용할 수 있다.
훅 등장 이래 대부분의 리액트 컴포넌트는 함수 컴포넌트로 작성되고 있을 정도로 많은 사랑을 받고 있다.
상태 관리용 훅
import {useState} from 'react'
const [state, setState] = useState(initialState);
useState 값으로 원시값을 넣는게 일반적이지만, 함수를 인수로 넣어줄 수도 있다. 리액트 팀은 초깃값이 복잡 혹은 무거운 연산 포함시에만 사용을 권장.
내부에 클로저가 존재하며, 클로저릍 통해 값을 가져오며 초깃값은 최초에만 사용된다는 것을 암.
import { useState, useEffect } from 'react'
export default function App() {
const [counter, setCounter] = useState(0);
function handleClick() {
setCounter((prev) => prev+1)
}
useEffect(() => {
function addMouseEvent() {
console.log(counter)
}
window.addEventListener('click', addMouseEvent)
// 클린업 함수
return() => {
console.log('클린업 함수 실행!', counter)
window.removeEventListener('click', addMouseEvent)
}
}, [counter])
return {
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+</button>
</>
}
}
결과는 다음과 같다.
클린업 함수 실행! 0
1
클린업 함수 실행! 1
2
클린업 함수 실행! 2
3
클린업 함수 실행! 3
4
이렇듯 useEffect는 언마운트라기보다는 함수 컴포넌트가 리렌더링 됐을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는, 말 그대로 이전 상태를 청소해 주는 개념으로 보는 것이 옳다.
빈 배열 vs 아무런 값 x vs 사용자가 직접 원하는 값을 넣어줄 수 있음
이존 의존성 배열과 현재 의존성 배열의 값에 하나라도 변경 사항이 있다면 callback으로 선언한 부수 효과 실행.
useMemo는 비용이 큰 연산에 대한 결과를 저장(메모이제이션)해 두고, 이 저장된 값을 반환하는 훅이다. 흔히 리액트에서 최적화를 떠올릴 때 가장 먼저 언급되는 훅이 바로 useMemo다.
useState의 차이점
function RefComponent() {
const inputRef = useRef()
// 이때는 미처 렌더링이 실행되기 전(반환되기 전)이므로 undefined를 반환
console.log(inputRef.current) //undefined
useEffect(() => {
console.log(inputRef.current) // <input type="text"></input>
}, [inputRef])
return <input ref={inputRef} type="text" />
}
즉 개발자가 원하는 시점의 값을 렌더링에 영향을 미치지 않고 보관해 두고 싶다면 useRef를 사용하는 것이 좋다.
props 전달 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있다.
다음과 같이 사용가능하다.
const Context = createContext<{ hello: string} | undefined>(undefined)
function Parentcomponent() {
return (
<>
<Context.Provider value={{ hello: 'react' }} >
<Context.Provider value={{ hello: 'javascript' }} >
<ChildComponent />
</Context.Provider>
<Context.Provider>
</>
)
}
function ChildComponent() {
const value = useContext(Context)
//react가 아닌 javascript가 반환
return <>{value ? value.hello : ''} </>
}
만약 여러 개의 Provider가 있다면 가장 가까운 Provider의 값을 가져오게 된다.
함수 컴포넌트 내부에서 사용할 때는 항상 컴포넌트 재활용이 어려워진다는 점을 염두에 둬야 한다.
루트에 전부 떤져놓는 방법도 있지만 현명한 접근법은 아니다.
또한 일부 리액트 개발자들이 콘텍스트와 useContext를 상태관리를 위한 리액트의 API로 오해하고 있는데 이는 그렇지 않다. 상태 관리 라이브러리가 되기 위해서는 최소 2가지 조건을 만족해야 한다.
콘텍스트는 둘 중 어느것도 하지 못하는 단순한 상태를 주입해 주는 API다.
useReducer는 useState의 심화 버전으로 볼 수 있다. useState와 비슷한 형태를 띠지만 좀 더 복잡한 상태값을 미리 정의해 놓은 시나리오에 따라 관리할 수 있다. useReducer에서 사용되는 용어를 먼저 살펴보자.
ex)
type State = {
count: number
}
// state의 변화를 발생시킬 action의 타입과 념겨줄 값(payload)를 정의
// 꼭 type과 payload라는 네이밍을 지킬 필요도 없으며, 굳이 객체일 필요도 없다.
// 다만 이러한 네이밍이 가장 널리 쓰인다.
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:
thorw new Error(`Unexpected action type ${action.type}`)
}
export default 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 만으로 수정할 수 있게 만듬으로써 사용을 제한하는 것이다.
내부 로직을 보면 useReducer나 useState 둘 다 세부 작동과 쓰임에만 차이가 있을 뿐, 결국 클로저를 활용해 값을 가둬서 state를 관리한다는 사실에는 변함이 없다.즉 useState와 useReducer 중 취사선택해 사용하면 된다.
실제 개발에서는 자주 볼 수 없는 훅으로 널리 사용되지 않는다. 하지만 일부 사례에서는 유용하게 활용될 수 있다.
ref는 useRef의 반환 객체로 HTMLElement에 접근하는 용도로 흔히 사용된다.하지만 상위에서 하위 컴포넌트로 이 ref를 전달할 수는 없다.
물론 이름을 바꿔서 ref를 전달할 수 있지만 일관성을 위해 forwardRef를 통해 전달하도록 하자.
const childComponent = forwardRef((props, ref) => {
})
function ParentComponent() {
const inputRef = Ref()
return (
<>
<input ref={inputRef} />
<ChildComponent ref={inputRef} />
</>
)
}
부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 훅이다.
ex)
const Input = forwardRef((props, ref)=> {
useImperativeHandle(
ref,
() -> ({
alert: () => alert(props.value),
}),
// useEffect의 deps와 같다.
[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} onChange={handleChange} />
<button onClick={handleClick}>Focus</button>
</>
)
}
웹서비스가 아닌 리액트 애플리케이션 개발과정에 사용. 디버깅하고 싶은 정보를 이 후엑다 사용하면 리액트 개발자 도구에서 볼 수 있음.
오직 다른 훅 내부에서만 실행할 수 있음. 컴포넌트 레벨에서는 실행시 작동 x