useState, useEffect with Closure

김윤진·2022년 6월 28일
0

React

목록 보기
5/13

[Closure] makes it possible for a function to have “private” variables.

let foo = 1;
function add() {
	foo = foo + 1;
	return foo;
}

console.log(add()); // 2

add 함수는 stateful function 이다.

여기서 문제는 foo 변수가 global scope에 위치하고 있으므로 foo 변수를 직접적으로 변경할 수 있는 문제가 있다.

let foo = 1;
function add() {
	foo = foo + 1;
	return foo;
}

console.log(add()); // 2
foo = 1000;
console.log(add()); // 1001

이 문제를 해결하기 위해 HOC 를 만들면 된다.

function getAdd() {
	let foo = 1;
	return function() {
		foo = foo + 1;
		return foo;
	}
}

const add = getAdd();
console.log(add()); // 2
console.log(add()); // 3
foo = 23; // ReferenceError: foo is not defined

이렇게 하면 stateful function을 만드는 동시에 function의 state를 안전하게 지킬 수 있다. 이것이 클로저를 활용한 것이다.

React의 useState를 처음부터 만들어보자

function useState(initVal) {
  let _val = initVal;
  const state = _val;
  const setState = newVal => {
    _val = newVal;
  }
  console.log(_val); // 1
  return [state, setState];
}

const [count, setCount] = useState(1);
console.log(count); // 1
setCount(2);
console.log(count); // 1

이렇게 하면 setCount를 하더라도 count값은 변경되지 않는다. 왜일까?

여기서 클로저와 스코프를 알아야 한다.

스코프는 함수가 호출할 때가 아니라 함수가 어디에 선언했는지에 따라 결정된다. 이를 렉시컬 스코핑이라고 한다. 위 예제의 함수 setState는 함수 useState 내부에서 선언되었기 때문에 함수 setState의 상위 스코프는 함수 useState 이다.

그리고 함수 setState 내에 있는 변수 _val의 스코프는 함수 setState이다. 그래서 함수 setState가 실행되더라도 함수 useState의 변수 _val는 변경되지 않고 그대로 처음의 initVal 값이 유지되는 것이다.

그렇다면 함수 useState 에서 변경된 값을 반환받으려면 어떻게 해야 할까?

function useState(initVal) {
  let _val = initVal;
  const state = () => {
    return	_val;
  };
  const setState = newVal => {
    _val = newVal;
  }
  console.log(_val);
  return [state, setState];
}

const [count, setCount] = useState(1);
console.log(count()); // 1
setCount(2);
console.log(count()); // 2

바로 함수 useState 에서 변수 _val 를 return 하는 함수를 만들어주면 된다. 왜 함수를 통해 값을 반환 받으면 setState에서 변경된 값이 반환되냐? 그것은 바로 함수 state 와 함수 setState 가 동일한 스코프를 가지고 있기 때문이다. 위에서 스코프는 함수가 호출할 때가 아닌 선언하는 곳에 따라 스코프가 결정된다고 했다. 함수 state 와 함수 setState 는 동일한 곳, 함수 useState 에서 선언되었기에 동일한 스코프를 가지고 있으므로 함수 setState 에서 값이 변경되더라도 함수 state 에서 변경된 값을 반환 받을 수 있다.

그리고 클로저 관점에서 설명하자면 함수 useState 는 내부 함수 countsetState 를 반환하고 생을 마감했다. 즉 함수 useState 는 실행된 후 콜스택(실행 컨텍스트 스택)에서 제거되었으므로 함수 useState 내부의 변수 _val 또한 더이상 유효하지 않게 되어 변수 _val 에 접근할 방법은 없어 보인다. 그러나 위 코드의 실행결과는 예상과는 다르다. 변수 _val 가 다시 살아난 것처럼 동작한다. 이처럼 자신을 포함하고 있는 외부함수보도 내부함수가 더 오래 유지되는 경우 외부 함수 밖에서 내부 함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저라고 부른다. 즉 클로저는 자신이 생성(선언)될 때의 환경을 기억하는 함수이다. 그리고 클로저에 의해 참조되는 함수 useState 의 변수 _val 를 자유 변수라고 부른다.

실행 컨텍스트 관점에서 설명하면 내부함수가 유효한 상태에서 외부함수가 종료되어 외부함수의 실행 컨텍스트가 반환되어도 외부함수 실행 컨텍스트 내의 활성 객체(Activation object)(변수, 함수 선언 등의 정보를 가지고 있다)는 내부함수에 의해 참조되는 한 유효하여 내부함수가 스코프체인을 통해 참조할 수 있다는 것을 의미한다. 즉 외부함수가 이미 반환되었어도 외부함수 내의 변수는 이름 필요로 하는 내부함수가 존재하는 한 계속 유지된다. 이때 내부함수가 외부함수에 있는 변수의 복사본이 아니라 실제 변수에 접근하는 것을 알고 있어야 한다.

이를 React처럼 만들어보자

const React = (function() {
  let _val;
  function useState(initVal) {
    const state = _val || initVal;
    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); // 1
App.click(); 
var App = React.render(Component); // 2

React.render 는 render 대신 console.log를 반환한다.

const React = (function() {
  let _val;
  function useState(initVal) {
    const state = _val || initVal;
    const setState = newVal => {
      console.log(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);
  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: 1, text: 1 }
App.type('Banana');
var App = React.render(Component); // { count: 'Banana', text: 'Banana' }

그러나 이 경우 함수 Component 에 state를 추가하면 하나의 store(변수 _val)만 존재하기 때문에 함수 setState 가 실행될때마다 상태 counttext 모두 변경된다.

const React = (function() {
  let hooks = [];
  let idx = 0;
  function useState(initVal) {
    const state = hooks[idx] || initVal;
    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 };
})();

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: 1, text: 'Apple' }
App.type('Banana');
var App = React.render(Component); // { count: 1, text: 'Apple' }

함수 useState 에서 idx에 1씩 더해주고 있기 때문에 React.render(Component) 를 한번 실행하면 변수 idx 는 2가 되기 때문에 변수 _idx 를 만들어줘야 한다.

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 };
})();

function Component() {
	const [count, setCount] = React.useState(1);
  const [text, setText] = React.useState('Apple');
	React.useEffect(() => {
		console.log('Hola');
	}, []);
  
  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: 'Apple' }
App.type('Banana');
var App = React.render(Component); // { count: 2, 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;
  }
  
  function useEffect(cb, depArray){
    const oldDeps = hooks[idx];
    let hasChange = true;
    	if (oldDeps) {
        hasChange = depArray.some((dep, i) => !Object.is(dep, oldDeps[i]))
      }
    	if (hasChange) cb(); 
    	hooks[idx] = depArray;
    	idx++;
  }
  
  return { useState, render, useEffect };
})();

function Component() {
	const [count, setCount] = React.useState(1);
  const [text, setText] = React.useState('Apple');
  React.useEffect(() => {
		console.log('Hola');
	}, []);
  return {
    render: () => console.log({ count, text }),
    click: () => setCount(count + 1),
    type: (word) => setText(word),
  }
}

var App = React.render(Component);
App.click(); 
var App = React.render(Component);
App.type('Banana');
App.type("Hola");
var App = React.render(Component);

함수 useEffect 는 dependence Array가 변경하면 실행되야 하기 때문에 dependence Array를 순회하면서 [Object.is](http://Object.is) 통해 이전 Array와 비교해서 실행여부를 결정한다.

0개의 댓글