React&Next.js

[글또] Closure의 원리를 이용해서 useState 구현하기(1)

jjin502 2024. 10. 27. 20:27


1. Closure란 함수가 선언 될 때, 렉시컬 환경을 기억해 함수가 해당 스코프 밖에서 실행되어도 그 환경에 접근할 수 있는 현상이다.
2. Closure의 핵심원리는 내부 함수의 생명주기가 끝났음에도 외부 환경의 변수에 대해 참조가 가능하다는 것이며 이를 활용해서 useState를 구현할 수 있다.
3. useState를 통해 데이터를 관리하고 유지할 수 있다.

 

배경

이번 면접의 사전 과제에서 Closure에 대한 개념과 어떻게 쓰이는지에 대한 내용을 작성하였다.

그리고 사전 과제를 기반으로, 기술 면접 진행 과정에서 useState를 구현하는 시간을 가졌다.

Closure을 아는 것에서 나아가 어떻게 활용되는지에 대해 추론해볼 수 있는 기회가 생겨 뜻깊었고,

이를 떠올려보면서 최대한 기록해보려고 한다.

 

Javascript와 떼어낼 수 없는 개념, Closure

Closure는 함수가 선언 될 때, 렉시컬 환경을 기억해

함수가 해당 스코프 밖에서 실행되어도 그 환경에 접근할 수 있는 현상이다.

 

  • 렉시컬 환경 : Code Block, Function, Script를 시작하기에 앞서 생성되는 객체로,
    실행할 스코프 내의 변수 및 함수를 프로퍼티로 저장하는 객체
  • 스코프 : 변수나 함수의 유효 범위

코드를 통해 조금 더 자세히 살펴보자. 

var exampleCounter = (function() {
  var privateNumber = 0;
  
  function changeBy(value) {
    privateNumber += value;
  }
  
  return {
    incrementNumber: function() {
      changeBy(+1);
    },
    decrementNumber: function() {
      changeBy(-1);
    },
    value: function() {
      return privateNumber;
    },
  };
})();

console.log(exampleCounter.value());

exampleCounter.incrementNumber();
exampleCounter.incrementNumber();

console.log(exampleCounter.value());

exampleCounter.decrementNumber();
console.log(exampleCounter.value());

 

위 코드의 출력값은 어떻게 될까?

 

privateNumber는 외부 함수의 변수이고,

changeBy 내부 함수에서 외부 함수의 변수에 접근할 수 있도록한다.

그리고 필요에 따라 3개의 내부 함수를 반환한다.

console.log를 통해 실행되면 반환되는 함수를 저장한다.

 

함수는 자기가 실행된 환경을 기억하기 때문에 호출된 이후에도 변수를 참조하거나 수정할 수 있다.

 

그래서 아래와 같이 출력될 것이다.

0
2
1

 

이러한 현상을 활용하면 2가지의 장점이 있다.

  1. 외부에서 변수에 직접 접근을 하는 것이 아니기 때문에 데이터를 보호할 수 있다.
  2. 특정 함수가 실행 될 때마다 변수를 초기화 시키지 않고 상태를 유지할 수 있다.

프론트엔드 관점에서 데이터를 보호하고 관리하는 것은 굉장히 중요하다.

그래서 우리가 흔히 알고 있는 useState를 활용해 Closure의 원리로 데이터를 관리 할 수 있다.

useState를 구현해보기

이제 이러한 원리를 바탕으로 useState를 간단하게 구현해보도록 하겠다.

 

React 환경에서 실행된다는 가정하에 코드를 작성해보았다.

const React = (function () {
  function useMyState(initValue) {
    let _val = initValue
    const state = () => _val
    const setState = (newVal) => {
      _val = newVal
    }

    return [state, setState]
  }

  return { useMyState }
})()

const [count, setCount] = React.useMyState(1)
console.log(count()) // 1
setCount(2)
console.log(count()) // 2

 

그리고 useState를 실행할 컴포넌트를 만든다면 이렇게 될 것이다.

function Component() {
  const [count, setCount] = React.useMyState(1)

  return {
    render: () => console.log(count),
    click: () => setCount(count),
  }
}

 

컴포넌트를 랜더링해주는 함수도 추가하자.

const React = (function () {
  function useMyState(initValue) {
    let _val = initValue
    const state = () => _val
    const setState = (newVal) => {
      _val = newVal
    }

    return [state, setState]
  }

  function render(Component) {
    const C = Component()
    C.render() // 컴포넌트 렌더링

    return C
  }

  return { useMyState, render }
})()

 

마지막으로 호출과 실행을 해주면된다.

var App = React.render(Component)
App.click()
var App = React.render(Component)

 

이렇게 실행했을 경우 아래와 같이 결과가 찍힐 것이다.

ƒ state() {}
ƒ state() {}

 

그 이유는 state를 반환함수로 내보내주고 있기 때문이다.

그래서 내부 변수를 밖으로 이동 시켜야한다.

 

const React = (function () {
  let _val

  function useMyState(initVal) {
    const state = _val || initVal
    const setState = (newVal) => {
      _val = newVal
    }

    return [state, setState]
  }

  function render(Component) {
    const C = Component()
    C.render()

    return C
  }

  return { useMyState, render }
})()

function Component() {
  const [count, setCount] = React.useMyState(1)

  return {
    render: () => console.log(count),
    click: () => setCount(count + 1),
  }
}

var App = React.render(Component) // 1
App.click()
var App = React.render(Component) // 2

 

이렇게 하면 useState를 선언하고 렌더링 한 후에 Click을 통한 state 변경이 잘 이뤄지는 것을 확인할 수 있다.

하지만 사실 이 코드도 어떠한 경우에는 정상적으로 동작하지 않는 경우가 있다.

 

그래서 다음 글에서는 어떤 경우에서 사용할 경우 정상적으로 동작하지 않는지,

어떻게 개선해야하는지, 왜 그렇게 개선해야하는지에 대해 다뤄볼 예정이다.