최근 자바스크립트의
클로저에 대해 공부하다가클로저를 활용하여상태관리를 할 수 있겠다는 생각이 들어, 기본적인상태관리를 할 수 있는 함수를 구현해봤다.
클로저는 사실 자바스크립트에만 존재하는 개념은 아니지만, 자바스크립트에서 굉장히 중요한 개념이다.
클로저의 사전적인 의미는 내부 함수(중첩함수)가 외부 함수 스코프를 참조하는 함수를 말한다.
자바스크립트는 모든 함수가 외부 함수의 스코프를 참조하고 있기 때문에 모두 클로저다. 하지만 통상적으로 외부 함수의 실행 컨텍스트가 실행 컨텍스트 스택(콜스택)에서 pop되어 나가 생명 주기가 끝났지만, 내부 함수가 외부함수(상위) 스코프의 식별자를 계속 참조하고 있는 함수만을 클로저라고 한다.
외부 함수 스코프의 식별자를 내부 함수에서만 접근할 수 있기 때문에 클로저를 활용하여 식별자의 값 변경을 제어할 수 있다는 특징이 있다. 값의 변경을 제어할 수 있다면, 응용하여 상태 관리에 활용해 볼 수 있겠다는 생각이 들었다.
리액트를 할 때는 별 생각 없이 자연스럽게 사용했었는데,상태 관리함수를 구현하다 보니 이거 완전 리액트랑 똑같자나? 라는 생각이 들어서 (물론 내부적으로는 더 복잡하겠지만) 네이밍을 리액트 hooks와 동일하게 지었다.
const [state, setState] = useState('abc');
const useState = (() => {
let state;
const setState = newState => state = newState;
return (newState) => {
setState(newState);
return [state, setState];
}
})();
우선 외부 함수의 생명주기가 내부 함수보다 짧게 하기 위해, 외부 함수를 즉시 실행 함수를 만들고 실행했다. 일반 함수라면 함수 스코프의 식별자는 함수가 호출된 이후에 참조할 수 없지만, 외부 함수 스코프의 식별자는 외부 함수의 호출에 의해 반환되는 내부 함수인 익명 함수가 useState 식별자에 담겨져 참조되어 지고 있다.
const [state, setState] = useState('before');
console.log(state); // 'before'
즉시 실행 함수가 반환하는 익명 함수가 배열을 반환하기 때문에 useState에는 배열이 값으로 담겨지고, state와 setState에 각각 배열의 원소가 비구조화 할당되어 담긴다.
state는 const 로 선언되었기 때문에 값을 재할당할 수 없지만, setState함수를 통해 state의 값을 변경할 수 있다. 그런데, 리액트는 setState함수가 호출되면 내부적으로 리렌더링를 통해 state의 값이 변환되지만, 변환된 값을 새로운 변수에 재할당하지 않고 변환된 값을 확인할 수가 없었다.
setState('after');
console.log(state); // 'before'
const useState = (() => {
let state;
const getState = () => state;
const setState = newState => state = newState;
return (newState) => {
setState(newState);
return [getState, setState];
}
})();
const [state, setState] = useState('before');
console.log(state()); // 'before'
setState('after');
console.log(state()); // 'after'
state의 값을 확인하기 위해 매번 함수를 호출해야 한다는 단점이 있지만, 이제 state를 은닉하여 setState를 통해서만 값을 변환할 수 있다. setState로 값을 변환한 값도 바로 확인할 수 있다.
const [state, setState] = useState(1);
console.log(state()); // 1
setState(state() + 1);
console.log(state()); // 2
const useState = (() => {
let state;
const getState = () => state;
const setState = newState => {
const stateType = typeof _state;
if( stateType !== 'undefined' && stateType !== typeof newState )
throw new Error(`${newState} is not ${stateType}`)
state = newState;
};
return (newState) => {
setState(newState);
return [getState, setState];
}
})();
const [state, setState] = useState('before');
console.log(state()); // 'before'
setState(100); // Error: 100 is not string
console.log(state());
함수는 자바스크립트로 구현했기 때문에 타입에 대한 확인을 하지 않아서, setState 함수 내부에서 이전 state와 변경하려는 state의 타입을 체크하는 로직을 별도로 추가했다.