함수 컴포넌트에서 상태 관리 및 생명주기 관리 등 다양한 작업을 위해 hook 이 추가되었다
함수 컴포넌트에서 핵심 개념으로 간결하게 작성할 수 있다
상태를 정의하고 관리할 수 있다
useState 를 사용하지 않고 변수로 데이터를 관리한다면 화면을 동적으로 관리할 수 없다
리액트의 렌더링은 return 과 이전 트리와 비교해 필요한 부분만 리렌더링 한다
리액트에서는 state 관리를 위해 클로저를 사용했다
일반 변수로 등록한다면 계속 초기화되기 때문이다
리액트 내부에서는 useReducer 를 이용해 구현되어 있다
useState 의 인수로 함수를 넘기는 것을 게으른 초기화라고 한다
state 가 처음 만들어질 때만 사용된다
초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 권장된다
const [count, setCount] = useState(() => {
// 복잡한 연산을 이 곳에 작성하면 첫 렌더링 시에만 실행된다
return 0
})
const handleClick = () => {
setCount(prev => prev + 1)
}
return (
<div>
<h1>{count}</h1>
<button onClick={handleClick}>+</button>
</div>
)
리액트의 함수는 렌더링 시마다 다시 실행되지만
useState 내부에 클로저가 존재하기 때문에 한 번만 실행된다
많은 비용이 드는 함수를 게으른 초기화로 사용한다면 클로저에 저장되어
한 번만 실행되도록 할 수 있다
localStorage 접근이나 배열 연산 등의 무거운 연산에 사용하면 좋다
클래스 컴포넌트의 생명주기 메서드와 비슷하게 사용할 수 있다
컴포넌트의 상태 및 값의 변화를 활용해 동기적으로 부수 효과를 만드는 훅이다
의존성에 있는 값에 변화가 생기면 실행되는 함수라고 볼 수 있다
클린업 함수는 렌더링 이후에 실행되지만 이전 state 를 참조해 실행된다
그렇기에 이벤트 리스너 remove 의 경우 클린업 함수에 작성하는 것이 맞다
그렇지 않으면 같은 이벤트 리스너가 여러개 등록될 것이다
클린업은 생명주기의 언마운트와는 차이가 있다
리렌더링됐을 때 이전의 값을 기준으로 실행하는,
이전 상태를 청소해주는 개념으로 봐야 한다
이런 특징을 이용해 언마운트 시 동작할 수 있도록 할 수 있을 뿐이다
useEffect 의 의존성 배열을 생략하면 렌더링 시 매번 실행된다
이러한 경우 일반 함수와 결과는 큰 차이가 없지만
client side 에서 동작함을 보장할 수 있고 컴포넌트가 모두 렌더링된 후에
실행된다는 점이 일반 함수와 다르다
서버 사이드 렌더링을 이용한다면 함수는 서버에서도 실행된다
useEffect 는 클라이언트 사이드에서 작동함을 보장한다
주로 useEffect 의 첫 번째 인수인 콜백을 익명 함수로 작성한다
복잡해진다면 기명함수로 사용하는 편이 가독성이 좋을 수 있다
useEffect(
function log() {
console.log('hi')
},
[])
useEffect 에 사용될 함수를 외부에 작성하면 가독성이 떨어질 수 있다
되도록 작게 만들고 내부에서 사용하자
useEffect 는 특정 상태나 값의 변화에 따라 부수효과로 실행되는 훅이기 때문에
동기적인 로직이 들어가야만 한다
비동기 함수가 들어가게 된다면 값이 뒤죽박죽 될 수 있다
이러한 경쟁상태를 막기 위해 비동기 사용을 막아놓은 것이다
콜백 내부에서 비동기 함수를 선언하거나
외부에서 선언된 비동기 함수를 가져다 쓰는 것은 가능하다
비동기 함수가 내부에 존재하게 될 때 비동기 함수의 생성, 실행을 반복하므로 클린업에서 비동기 함수에 대한 처리를 추가하는 것이 좋다
비용이 큰 연산에 대한 결과 값을 메모이제이션 해 두고 이 저장된 값을 반환한다
리액트 최적화에 가장 먼저 언급되는 훅
컴포넌트를 메모이제이션 할 수 있지만
그럴 때는 React.memo 로 작성하는 편이 좋다
MemoizedComponent 는 의존성으로 선언된 값이 변경되지 않는 한 리렌더링이 일어나지 않는다
메모이제이션은 연산 비용이 저장 비용보다 클 때 사용한다
useMemo 가 값을 저장한다면 useCallback 은 콜백함수 자체를 기억한다
리렌더링 시 새로 만들어지는 함수를 새로 만들지 않고 기억한다
의존성으로 선언된 값이 변경되었을 때만 함수가 재생성 된다
useCallback 도 useEffect 와 마찬가지로 기명 함수를 사용하면
가독성에 도움이 될 수 있다
React.memo() 사용 시에 props 로 함수가 전달된다면 상위 컴포넌트가 리렌더링될 때 함수가 새로 생성되기 때문에 같이 렌더링이 일어난다
이런 경우 useCallback 으로 함수를 기억하게 되면
불필요한 레더링을 방지할 수 있다
렌더링이 일어나도 변경 가능한 상태값을 저장한다
useState 와는 다르게 변경이 일어나도 렌더링을 발생시키지 않는다
컴포넌트가 렌더링될 때만 값이 생성되고
컴포넌트 인스턴스가 여러개라도 각각 별개의 값을 가진다
주로 DOM 에 직접 접근할 때 사용된다
리액트 애플리케이션은 부모와 자식으로 이루어진 트리 구조를 갖고 있기 때문에
데이터를 자식에게 넘겨줄 때 props 를 사용한다
자식이 여럿일 경우 깊이가 깊어저 props drilling 이 복잡하고 귀찮아진다
이러한 것을 극복하기 위해 등장한 개념이 context
명시적인 전달 없어도 선언한 하위 컴포넌트 모두 원하는 값을 사용할 수 있게 된다
const Context = createContext<{ hello: string } | undefined>(undefined)
const Parent = () => {
return (
<>
<Context.Provider value={{ hello: 'react' }}>
<Context.Provider value={{ hello: 'js' }}>
<Child />
</Context.Provider>
</Context.Provider>
</>
)
}
const Child = () => {
const value = useContext(Context)
return <h1>{value ? value.hello : ''}</h1> // value = 'js'
}
useContext 는 함수 컴포넌트에서 Context 를 사용할 수 있도록 만들어진 훅이다
상위 컴포넌트에서 만들어진 Context 를 Provider 를 이용해
하위 컴포넌트에 전달할 수 있고 useContext 로 값을 사용할 수 있다
가장 가까운 Provider 의 value 를 사용하게 된다
다수의 Provider 와 useContext 를 사용한다면
함수로 감싸서 사용하는 편이 좋다
타입 추론에도 조고 상위에 Provider 가 없는 경우에도 에러 추적에 유용하다
useContext 를 사용하게 되면 컴포넌트 재활용이 어려워진다
상위 Provider 에 의존성을 갖게 되기 때문이다
useContext 는 상태 관리를 위한 훅이 아니고
상태 주입만 가능한 훅이니 유의해야 한다
상태 관리의 조건은
1. 어떤 상태를 기반으로 다른 상태를 만들 수 있어야 한다
2. 필요에 따라 상태 변화를 최적화 할 수 있어야 한다 (렌더링 최적화 등)
useMemo 를 함께 이용해서 최적화 할 수 있다
useReducer 는 useState 의 심화 버전으로 볼 수 있다
복잡한 상태 값을 미리 정의한 시나리오에 따라 관리할 수 있다
(필요한 적정 시기에 상태를 의도적으로 업데이트 할 수 있다)
반환값은 useState 와 같이 길이가 2인 배열이다
첫 번째 반환값은 현재 state 를 의미한다
두 번째 반환값은 dispatcher 로 stale 을 업데이트 하는 함수다
setState 는 상태를 넘겨주지만 useReducer 는 action 을 넘겨준다
action 은 state 을 변경할 수 있는 함수다
useReducer 의 인수는 3개로
action 을 정의하는 함수, 초깃값, 게으른 초기화에 사용될 함수
3번째는 옵션이다
// useReducer 가 사용할 state
type State = {
count: number
}
// state 변화를 발생시킬 action 의 타입과 넘겨줄 payload(값) 을 정의
type Action = {
type: 'up' | 'down' | 'reset';
payload?: State
}
// 무거운 연산이 필요한 게으른 초기화 함수 (옵션)
const init = (count: State): State {
// State 를 받아서 초깃값을 정의할 때 무거운 연산이 필요하다면
return count
}
const initialState: State = { count: 0 }
const reducer = (state: State, action: Action): State {
switch (action.type) {
case 'up':
return { count: state.count + 1 }
case 'down':
return { count: state.count -1 }
case 'down':
return { count: state.count -1 }
default:
throw new Error(`Unexpected action type ${action.type}`)
}
}
const App = () => {
const [state, dispatcher] = useReducer(reducer, initialState, init)
const hanleUpButtonClick = () => {
dispatcher({ type: 'up' })
}
const hanleDownButtonClick = () => {
dispatcher({ type: 'up' })
}
const hanleResetButtonClick = () => {
dispatcher({ type: 'reset', payload: '1' })
}
}
실제 개발 단계에서는 자주 사용되지 않지만 일부 유용한 사용 사례가 있다
React.forwardRef 를 이해하고 있어야 사용 가능
ref 는 useRef 에서 반환된 객체로 HTMLElement 에 접근하는 용도로 자주 사용된다
ref 를 props 로 하위 컴포넌트에 넘겨줄 때 ref 를 전달하는 일관성을 제공하기 위해 사용된다
useImpreativeHandle 은 부모에게서 넘겨받은 ref 를 수정할 수 있는 훅이다
useEffect 와 기능은 같지만
모든 DOM 변경 후에 동기적으로 작동한다
useEffect 는 DOM 변경 이전에 비동기적으로 작동하지만
완전히 UI 가 렌더링 된 이후에 작동하게 할 수 있는게 useLayoutEffect
리액트 훅은 몇 가지 규칙이 존재한다
이러한 규칙을 rules-of-hooks 라고 하며
ESlint 규칙에도 react-hooks/rules-of-hooks 가 있다
리액트에서 재사용 로직을 관리할 수 있는 두 가지 방법은
사용자 정의 훅과 고차 컴포넌트다
서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때
이름을 use 로 시작해야 훅의 규칙을 적용받을 수 있다
useState 나 useEffect 등의 조합으로 사용자 정의 훅을 만든다
고차 컴포넌트(HOC) 는 컴포넌트 자체 로직을 재사용하기 위한 방법이다
사용자 정의 훅은 리액트 훅을 기반으로 하기 때문에 리액트에서만 사용할 수 있는 기술이지만
고차 컴포넌트는 고차 함수의 일종으로 자바스크립트 환경에서도 사용 가능하다
React.memo 가 이런 고차 컴포넌트에 해당한다
props 의 변화가 없음에도 컴포넌트가 렌더링 하는 것을 방지하기 위해 만들어진 고차 컴포넌트다
React.memo 는 렌더링 이전 props 의 변화가 없으면 이전에 기억해둔 컴포넌트를 반환한다
고차 함수는 배열 함수가 가장 자주 쓰인다
const list = [1, 2, 3]
const dubleList = list.map(() => item * 2)
고차 함수는 함수를 인수로 받는다
function add(a) {
return function (b) {
return a + b
}
}
const result = add(1)
const result2 = result(2) // 3
add(1) 에서 함수를 호출하는 시점에 정보가 a 에 포함되고
이러한 정보가 담긴 함수를 result 에 받는다
useState 와 비슷하다
useState 의 실행은 함수 호출 시점에 끝나지만 state 의 값은 클로저에 기억된다
고차 함수를 활용하면 함수를 인수로 받거나 새로운 함수를 반환해 새로운 결과를 만들 수 있다
interface LoginProps {
loginRequired?: boolean
}
const withLoginComponent<T> = (Component: ComponentType<T>) => {
return (props: T & LoginProps) => {
const { loginRequired, ...restProps} = props
if (loginRequired) <>로그인이 필요합니다</>
return <Component {...(restProps as T)} />
}
}
const Component = withLoginComponent(props: {value: string}) => {
return <h1>{props.value}</h1>
}
export default function App() {
const isLogin = true
return <Component vlaue="text" loginRequired={isLogin} />
}
평범한 Component 를 고차 컴포넌트 withLoginComponent 로 감싸줬다
withLoginComponent 는 함수 컴포넌트를 인수로 받아서 컴포넌트를 반환한다
고차 컴포넌트는 컴포넌트 전체를 감쌀 수 있다는 점에서 사용자 정의 훅보다
더 큰 영향력을 발휘할 수 있다
고차 컴포넌트는 with 라는 이름으로 시작해야 한다
이는 훅 규칙은 아니고 리액트 커뮤니티의 관습이다
고차 컴포넌트는 부수효과를 최소화 해야 한다
또한 임의로 인수로 받는 컴포넌트의 props 를 변경해서는 안된다
훅으로만 공통 로직을 처리할 수 있다면 사용자 정의 훅을 사용하는 것이 좋다
훅 자체로는 렌더링에 영향을 미치지 못하기 때문에
사용기 제한적이므로 반환하는 값을 바탕으로 무엇을 할지 개발자에게 달려있다
컴포넌트에 미치는 영향을 최소화할 수 있다
공통적으로 처리해야할 에러나 로그인 등 애플리케이션 전반에 나타나는 일들은
사용자 정의 훅보다는 고차 컴포넌트가 좋다
렌더링 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하는 편이 좋다
공통화된 렌더링 로직을 처리하기에 좋다