[js] 클로저를 활용한 상태 관리

김효식 (HS KIM)·2021년 12월 7일
1

최근 자바스크립트의 클로저에 대해 공부하다가 클로저를 활용하여 상태관리를 할 수 있겠다는 생각이 들어, 기본적인 상태관리를 할 수 있는 함수를 구현해봤다.

클로저란?

클로저는 사실 자바스크립트에만 존재하는 개념은 아니지만, 자바스크립트에서 굉장히 중요한 개념이다.
클로저의 사전적인 의미는 내부 함수(중첩함수)외부 함수 스코프를 참조하는 함수를 말한다.

자바스크립트는 모든 함수가 외부 함수의 스코프를 참조하고 있기 때문에 모두 클로저다. 하지만 통상적으로 외부 함수실행 컨텍스트실행 컨텍스트 스택(콜스택)에서 pop되어 나가 생명 주기가 끝났지만, 내부 함수외부함수(상위) 스코프식별자를 계속 참조하고 있는 함수만을 클로저라고 한다.

외부 함수 스코프식별자내부 함수에서만 접근할 수 있기 때문에 클로저를 활용하여 식별자의 값 변경을 제어할 수 있다는 특징이 있다. 값의 변경을 제어할 수 있다면, 응용하여 상태 관리에 활용해 볼 수 있겠다는 생각이 들었다.

상태 관리

리액트를 할 때는 별 생각 없이 자연스럽게 사용했었는데,상태 관리함수를 구현하다 보니 이거 완전 리액트랑 똑같자나? 라는 생각이 들어서 (물론 내부적으로는 더 복잡하겠지만) 네이밍을 리액트 hooks와 동일하게 지었다.

const [state, setState] = useState('abc');

위 코드는 리액트에서 `useState` 훅을 사용하여 `state`에 값을 할당하는 방법이다. 일반적으로 `state` 대신 다른 식별자 이름을 사용하겠지만, 여기서는 `state`로 했다. `state`를 통해서 값에는 접근할 수 있지만 `setState`함수를 통해서만 값을 변경할 수 있다.
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에는 배열이 값으로 담겨지고, statesetState에 각각 배열의 원소가 비구조화 할당되어 담긴다.


stateconst 로 선언되었기 때문에 값을 재할당할 수 없지만, setState함수를 통해 state의 값을 변경할 수 있다. 그런데, 리액트setState함수가 호출되면 내부적으로 리렌더링를 통해 state의 값이 변환되지만, 변환된 값을 새로운 변수에 재할당하지 않고 변환된 값을 확인할 수가 없었다.

setState('after');
console.log(state); // 'before'

별도로 `render`함수와 `Component`를 만들어서 값을 변경하는 방법을 생각해 볼 수 있겠지만, `클로저`를 활용하여 이번에는 `상태 관리`를 하는 것에 포커싱을 하고 다른 방법을 생각해 봤다.
const useState = (() => {
  let state;
  const getState = () => state;
  const setState = newState => state = newState;
  return (newState) => {
    setState(newState);
    return [getState, setState];
  }
})();

다양한 방법들을 시도해봤지만, 내부적인 `렌더링 과정`을 생략하고 새로운 값을 기존의 `state`에 넣을 수 있는 방법을 생각해내지 못했다. 그래서 기존에 `상태`를 관리하던 식별자 대신 식별자의 값을 반환하는 `함수`를 반환하는 방식으로 변경했다.
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

기존의 `state` 값을 참조하여 숫자를 늘리거나 줄일 수도 있다.

결과

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타입을 체크하는 로직을 별도로 추가했다.

profile
자기개발 :)

0개의 댓글