React Hook의 동작
React Hook의 동작에 대해
2021-11-07
React Hook
React로 개발할 땐 클래스형 컴포넌트 보다 React 16.8 부터 도입된 Hook 기반 함수형 컴포넌트를 더 선호하여 개발하는 것 같다.
기존 클래스형 컴포넌트는 여러 단계의 상속과 복잡성, 오류 등이 많았지만 Hook이 도입되면서 클래스형 컴포넌트가 가지고 있는 기능을 모두 사용할 수 있음은 물론이고 복잡성과 재사용성의 단점들까지도 해결됐다.
클로저(Closure)
Hook의 핵심은 JS의 클로저이다. 클로저는..봐도봐도 잘 이해가 가지 않는다ㅠㅠ
클로저 = 함수 + 함수를 둘러싼 환경
이라고 말할 수 있는데, 자바스크립트는 함수 안에서도 함수를 선언할 수 있다.
먼저 선언된 함수를 외부함수, 이후에 선언된 함수를 내부함수라 한다면 기본적으로 내부함수는 외부함수의 요소에 접근이 가능하다.
즉 외부함수의 변수에 내부함수의 변수가 접근 할 수 있는 자바스크립트의 메커니즘이다.
function outerFn() {
let outerVar = 'outer'
console.log(outerVar)
//클로저 함수
function innerFn() {
let innerVar = 'inner'
console.log(innerVar)
}
//클로저 함수 안에서는
//지역변수(innerVar)
//외부함수의 변수(outerVar)
//전역변수(globalVar)
//접근이 모두 가능하다.
return innerFn
}
let globalVar = 'global'
let innerFn = outerFn()
innerFn()
useState
function useState(initVal) {
let _val = initVal
const state = _val
const setState = newVal => {
_val = newVal
}
return [state, setState]
}
const [count, setCount] = useState(1)
console.log(count) // 1
setCount(2)
console.log(count) // 1 (?)
위 함수에서 count
는 한번 가져오고 끝난 값이기 때문에 즉각적으로 바뀌지 않는다.
만약 const state = _val
부분을 함수로 바꾸고, 값을 쓰는게 아닌 호출해주는 식으로 바꾼다면 호출할 때마다 값을 가져오기 때문에 setCount
가 반영된 값을 가져올 수 있다.
// useState 안에서
// ...
const state = () => _val
// ...
const [count, setCount] = useState(1)
console.log(count()) // 1
setCount(2)
console.log(count()) // 2
state
는 상단에 정의된 _val
를 반환하고, setState
는 전달 된 매개변수 newVal
를 지역 변수로 설정한다.
함수형 컴포넌트에서 사용하기
const React = (function() {
function useState(initVal) {
let _val = initVal
const state = _val
const setState = newVal => {
_val = newVal
}
return [state, setState]
}
function render(Component) {
const C = Component()
C.render()
return C
}
return { useState, render }
})()
function Component() {
const [count, setCount] = React.useState(1)
return {
render: () => console.log(count),
click: () => setCount(count + 1),
}
}
// 아직까진 중간 과정이므로 제대로 동작하지 않는다.
var App = React.render(Component)
App.click()
var App = React.render(Component)
모듈패턴을 이용해 React
라는 네임스페이스에 useState
를 집어넣는다.
그리고 DOM을 사용하진 않지만 가상의 컴포넌트를 만들어 useState
훅을 가져다 쓰는 방식이다.
여기서 count
가 제대로 동작하게 만들기 위해 _val
로 쓰고 있던 변수를 React
내부로 끌어올리면 랜더링 이후 클릭해도 작동한다.
const React = (function() {
let _val
function useState(initVal) {
const state = _val || initVal
// ...
}
// ...
})()
var App = React.render(Component) // 1
App.click()
var App = React.render(Component) // 2
App.click()
var App = React.render(Component) // 3
App.click()
var App = React.render(Component) // 4
여러 개의 훅
하지만 실제로 하나의 컴포넌트에서 여러 상태를 관리하기 위해 여러 훅을 사용하는데. _val
하나에 의존한 지금 상태로 useState
를 두번 호출하게되면
function Component() {
const [count, setCount] = React.useState(1)
const [text, setText] = React.useState('apple')
return {
render: () => console.log({ count, text }),
click: () => setCount(count + 1),
type: word => setText(word),
}
}
var App = React.render(Component) // {count: 1, text: 'apple'}
App.click()
var App = React.render(Component) // {count: 2, text: 2}
App.type('banana')
var App = React.render(Component) // {count: 'banana', text: 'banana'}
중간에 값이 덮어씌워 진다. 이를 관리 하려면 각 값별로 배열에 담아 다루면 된다.
const React = (function() {
let hooks = []
let idx = 0
function useState(initVal) {
const state = hooks[idx] || initVal
const _idx = idx // 이 훅이 사용해야 하는 인덱스를 가둬둔다.
const setState = newVal => {
hooks[_idx] = newVal
}
idx++ // 다음 훅은 다른 인덱스를 사용하도록 한다.
return [state, setState]
}
function render(Component) {
idx = 0 // 랜더링 시 훅의 인덱스를 초기화한다.
const C = Component()
C.render()
return C
}
return { useState, render }
})()
여기까지보면 왜 Hook에 기본 규칙이 있는지 알 수 있다.
- 최상위(Top Level)에서만 Hook을 호출
- 오직 React 함수 내에서 Hook을 호출
useEffect
useEffect
를 사용하면 컴포넌트를 화면에 그린 후 실행될 함수를 정의할 수 있다.
또 매번 render
했을 때 최초 한 번만 실행되며 매 업데이트마다 실행된다.
function Component() {
const [count, setCount] = React.useState(1)
const [text, setText] = React.useState('apple')
// 랜더링 시 최초에 한 번만 실행된다.
// 배열 안에 관찰하고자 하는 상태를 전달하면 그 상태에 반응하여 콜백이 실행된다.
React.useEffect(() => {
console.log('side effect')
}, [])
// ...
}
function useEffect(cb, depArray) {
const oldDeps = hooks[idx] // 이미 저장되어있던 의존 값 배열이 있는지 본다.
let hasChanged = true
if (oldDeps) {
// 의존 값 배열의 값 중에서 차이가 발생했는지 확인한다.
// 실제로 리액트 구현체도 `Object.is` 로 값을 비교한다. 정확한 동작은 MDN 참고.
hasChanged = depArray.some((dep, i) => !Object.is(dep, oldDeps[i]))
}
// 값이 바뀌었으니 콜백을 실행한다.
if (hasChanged) {
cb()
}
// useEffect도 훅의 일부분이다. hooks 배열에 넣어서 관리해준다.
hooks[idx] = depArray
idx++
}
위에 선언된 React
모듈 안에 useEffect
함수를 정의한다.
두번째 인자로 넣어둔 의존배열(dependency array)을 관찰하며 값이 변하면 콜백을 실행하고, 그렇지 않으면 실행하지 않는다.
참조
위 글은 아래 두 블로그를 정독하며 정리하였습니다.
Deep dive: How do React hooks really work?
Getting Closure on React Hooks