React Hooks는 함수형 컴포넌트에서도
상태, 컴포넌트의 생명주기 등 React의 기능들을 사용할 수 있게 만들어준다.
React의 공식 문서에 보면, Hooks의 규칙이 소개되어있다.
use로 시작하는 함수인 Hook을 최상위 레벨에서만 호출하도록 되어있다.
특히, 조건문이나 반복문 내부, 그리고 조건부 return
문 이후에 Hook을 호출하는 것은 막혀있다.
이는, React가 렌더링 시에 Hook의 호출 순서를 기반으로 내부 상태를 관리하기 때문이라고 한다.
즉, 조건문이나 반복문 등에서 훅의 호출 순서가 달라지면, 각 렌더링에서 어떤 훅이 어떤 상태와 연결되는지를 예측할 수 없게 되어 문제가 발생할 수 있다는 것이다.
느낌으로는 알겠지만, 아직 어렵다. 직접 구현해보면서 따라해본다.
일단 useState를 간단하게 만들어보자. 실제와 다르다.
const ReactX = (() => {
const useState = (initialValue) => {
let state = initialValue
const setState = (newValue) => {
state = newValue
}
return [state, setState]
}
return { useState }
})()
이 useState는 문제가 있다. 이 방식은 상태가 컴포넌트 호출 사이에 유지되지 않는다.
위 ReactX 아래에 다음과 같이 작성했다.
const { useState } = ReactX
const Component = () => {
const [counter, setCounter] = useState(1)
console.log(counter)
if (counter !== 2) {
setCounter(2)
}
}
Component() // 1차 호출
Component() // 2차 호출
1이 출력된 이후에 2가 출력될 것 같지만, 모두 1이 출력된다.
이는, useState 내부의 state가 함수가 호출될 때마다 새로 생성되는 지역 변수이기 때문이다.
따라서, 카운터를 2로 변경해도, 함수가 종료되면서 사라지기 때문에, 다시 1로 초기화된다.
state
변수를 외부로 이동시키면 이 문제를 해결할 수 있다.
const ReactX = (() => {
let state
const useState = (initialValue) => {
if (state === undefined) {
state = initialValue
}
const setState = (newValue) => {
state = newValue
}
return [state, setState]
}
return { useState }
})()
const { useState } = ReactX
const Component = () => {
const [counter, setCounter] = useState(1)
console.log(counter)
if (counter !== 2) {
setCounter(2)
}
}
Component() // 1 출력
Component() // 2 출력
하지만, 단 하나의 상태만 존재할 수 있게 되기 때문에 여전히 문제가 존재한다.
실제 React는 상태가 무한히 가능하기 때문에 이를 배열로 관리하고,
index
를 사용해 접근할 수 있도록 변경한다.
const ReactX = (() => {
let state = []
let index = 0
const useState = (initialValue) => {
const localIndex = index
index++
if (state[localIndex] === undefined) {
state[localIndex] = initialValue
}
const setState = (newValue) => {
state[localIndex] = newValue
}
return [state[localIndex], setState]
}
const resetIndex = () => {
index = 0
}
return { useState, resetIndex }
})()
const { useState, resetIndex } = ReactX
const Component = () => {
const [counter, setCounter] = useState(1)
console.log(counter)
if (counter !== 2) {
setCounter(2)
}
}
Component() // 1차 호출
resetIndex()
Component() // 2차 호출
이제 React 의 useState와 비슷하게 동작하도록 만들 수 있다.
React Hook의 상태 관리는 사실 연결 리스트를 사용하고 있다.
이 배열로 만든 Hook과 마찬가지로 항상 동일한 순서로 Hook이 호출되어야 하는 것은 당연하다.
실제로 1, 2 가 차례로 출력된다.
이때, 알맞는 상태에 접근하기 위해 localIndex
를 사용하도록 했다.
만약, 조건문이나 반복문에 Hook의 호출이 이루어진다면, 매 렌더링에 이 localIndex
의 값이 달라질 것이고, 그렇다면 의도대로 상태에 접근할 수 없을 것이다.
이번엔 useEffect다.
state
를 hooks
로 이름만 변경한 후, useEffect를 만들었다.
const ReactX = (() => {
let hooks = []
let index = 0
const useState = (initialValue) => {
// ...
}
const resetIndex = () => {
index = 0
}
const useEffect = (callbackFn, dependencies) => {
let hasChanged = true
const oldDependencies = hooks[index]
if (oldDependencies) {
hasChanged = false
dependencies.forEach((dependency, index) => {
const oldDependency = oldDependencies[index]
const areSame = Object.is(dependency, oldDependency)
if (!areSame) {
hasChanged = true
}
});
}
if (hasChanged) {
callbackFn()
}
hooks[index] = dependencies
index++
}
return { useState, useEffect, resetIndex }
})()
const { useState, useEffect, resetIndex } = ReactX
const Component = () => {
useEffect(() => {
console.log('Effect')
}, [])
}
Component() // 1차 호출
resetIndex()
Component() // 2차 호출
resetIndex()
Component() // 3차 호출
그리고, 결과를 보면 최초에만 실행되는 것을 볼 수 있다.
마찬가지로, 컴포넌트를 아래와 같이 바꾸면, 의존성 배열 내의 값이 변경될 때만 동작함을 알 수 있다.
const Component = () => {
const [counter, setCounter] = useState(1)
const [changed, setChanged] = useState(false)
console.log(counter)
useEffect(() => {
console.log('Effect')
}, [changed])
if (counter !== 2) {
setCounter(2)
}
if (!changed && counter === 2) {
setChanged(true)
}
}
Component() // 1차 호출
resetIndex()
Component() // 2차 호출
resetIndex()
Component() // 3차 호출
카운터는 세 번 출력되지만, useEffect는 최초와 changed가 변경될 때만 동작함을 알 수 있다.
이렇게 useState와 useEffect의 구조를 보면 Hook의 규칙을 이해하는 것이 쉽다.
이는, React가 상태와 의존성 배열을 순서로 관리하기 때문이다.
즉, React는 내부적으로 Hook들을 배열(사실은 연결 리스트인 것 같다)로 관리하는데,
렌더링 시 Hook의 호출 순서가 일정해야 각 Hook과 올바른 상태, 의존성 배열을 연결할 수 있기 때문이다.
다음으로는 Concurrent Mode 에 대해 더 알아본다.
모던 리액트 Deep Dive
How Do React Hooks Actually Work? React.js Deep Dive #3