공식문서에 따르면, 리엑트의 훅이란 기존 클래스 컴포넌트에서 사용하던 생명주기와 관련된 상태와 함수들을 함수 컴포넌트에서도 사용이 가능하도록 연동시킨 함수이다.
기존에는 이 생명주기와 관련된 함수들을 사용할 때 몹시 리펙토링과 디버깅에 어려움을 많이 겪었는데, 역시나 공식문서에 작성된 내용에 따르면
생명주기 함수의 내부 로직에 대한 구현의 자유성은 그만큼 개발자들마다 제각각 자신만의 생각으로 생명주기 함수의 로직을 짜다 보니 특정 생명>주기에 맞지 않는 명렁문들이 함수 몸체에 들어가는 등의 혼선이 빚어지고
특히나 클래스에서 사용되는 "this" 키워드는 아무리 클래스 컴포넌트의 작성에 익숙한 엔지니어라 하더라도 무엇을 지칭하는지에 대한 혼선이 빚어지며 잦은 에러로 이어진다는 말이었다.
이를 위해 함수 컴포넌트를 제작하면서 클래스 컴포넌트에서 사용하던 생명주기 함수들을 대체하기 만들기 위해 만들어진 것이 hook이다.
useState는 클래스 컴포넌트의 this.state과 연동되는 부분이다. 간략하게 표현되는 useState의 내부구현은 아래와 같다
const MyReact = (function() {
let hooks = [],
let currentHook = 0
return {
render(Component) {
const Comp = Component() // 이펙트들이 실행된다.
Comp.render()
currentHook = 0 // 다음 렌더를 위해 초기화
return Comp
},
useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue;
const setStateHookIndex = currentHook ; //클로져 내에서 고정
const setState = (newState) => (hooks[setStateHookIndex] = newState);
return [hooks[currentHook++], setState]
},
}
})()
훅들은 본질적으로 모두 클로져를 핵심개념으로 사용한다.
클로져란 주변의 렉시컬 스코프 환경을 기억하면서 리턴되는 함수를 뜻한다.
useState을 호출하게 되면, MyRepeat의 즉시실행함수 스코프 범위내에 존재하는 hooks의 배열이 상태값을 관리하고,
currentHook는 useState를 통해 상태가 형성이 될 때마다 추적하기 위한 아이디값이 된다.
useState이 리턴하게 되는 배열 내의 setter은 외부 스코프를 다 참조할수 있기 때문에 해당 내용을 기반으로 배열을 변경시킬 수 있다.
useEffect는 생명주기에서 componentDidMount, componentDidUpdate, componentWillUnmount를 연동한 hook이다.
이 useEffect의 pseudo 구현은 아래와 같다
const MyReact = (function() {
let hooks = [],
let currentHook = 0
return {
render(Component) {
const Comp = Component() // 이펙트들이 실행된다.
Comp.render()
currentHook = 0 // 다음 렌더를 위해 초기화
return Comp
},
useState(initialValue) {
hooks[currentHook] = hooks[currentHook] || initialValue;
const setStateHookIndex = currentHook ; //클로져 내에서 고정
const setState = (newState) => (hooks[setStateHookIndex] = newState);
return [hooks[currentHook++], setState]
},
useEffect(callback, depArray) {
const hasNoDeps = !depArray //디펜던시 배열이 존재하는가 안하는가
const deps = hooks[currentHook]
const hasChangedDeps = deps ? depArray.every((el, i) => el === deps[i]) : false
if (hasNoDeps || hasChangedDeps) {
callback()
hooks[currentHook] = depArray
}
currentHook++ // 이 Hook에 대한 작업 완료
},
}
})()
useEffect의 경우, 전달받는 의존성 배열에 대해 렉시컬 스코프에 존재하는 값들을 비교해가면서 현재 dependency 배열에 해당하는 값이 존재하지 않거나 애초부터 dependency 배열이 없다면 callback을 실행하도록 되어있다.
사실 위 내용을 타 사이트에서 참조하긴 했지만 아무래도 흉내내기에 가깝다보니 정교하지 않다. 예를들어, 해당 내용대로면 랜더링시에 렉시컬 스코프에 존재하는 상태리스트들은 값이 존재하는것이지 dependency 배열이 들어가있지 않으므로 deps에서 에러를 낼 것이다. 다만 클로져를 최대한 활용한다는 점을 이해하면 좋을것같다.