useState == 클로저?? || React Hooks useState의 동작원리

guddls ju·2022년 9월 8일
0

React Hooks

목록 보기
1/3
post-thumbnail

참고

https://ko.reactjs.org/docs/hooks-state.html

https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Make-useSate-hook/


React Hook의 기본이자 가장 많이 사용하는 useState에 대해 정리하면서 씹고 뜯고 맛봐보자

💡useState

const [state, setState] = useState(initialState);

useState함수를 호출하면 파라미터로 전달한 initialState값을 초기값으로 갖는
state(상태 변수)와 setState(변수를 수정할 수 있는 setter 함수)를 배열로 반환한다.


👍useState를 호출하는 것은 무엇을 하는 걸까?

“state 변수”를 선언할 수 있다. (위에 선언한 변수는 state라고 부르지만 banana처럼 아무 이름으로 지어도 된다) 일반 변수는 함수가 끝날 때 사라지지만, state 변수는 React에 의해 사라지지 않습니다.


useState의 인자로 무엇을 넘겨주어야 할까?*

 useState()Hook의 인자로 넘겨주는 값은 state의 초기 값으로 숫자 타입과 문자 타입을 가질 수 있다. (2개의 다른 변수를 저장하기를 원한다면 useState()를 두 번 호출해야 함)


👌useState는 무엇을 반환할까?

[state 변수, 해당 변수를 갱신할 수 있는 함수] 이 두 가지 쌍을 반환한다. 이는 useState Hook을 const [count, setCount] = useState() 처럼 쓰는 이유이다.


+ 왜 createState가 아닌, useState로 이름을 지었을까?

컴포넌트가 렌더링할 때 오직 한 번만 생성되기 때문에 “Create”라는 이름은 꽤 정확하지 않을 수 있습니다. 컴포넌트가 다음 렌더링을 하는 동안 useState는 현재 state를 줍니다. - React 공식문서


💡그렇다면 이렇게 간단하게 사용하고 있는 useState의 동작원리는 무엇일까?

useState의 동작을 살펴보면

function Counter () {
  const [count, setCount] = useState(1);

  // 돔에서 직접 호출하기 위해 window(전역객체)에 할당
  window.increment = () => setCount(count + 1);

  return `
    <div>
      <strong>count: ${count} </strong>
      <button>증가</button>
    </div>
  `;
}
  1. useState로 state와 setState를 만들 수 있다.
  2. 500ms(0.5초)마다 setCount를 실행한다.
  3. 값이 1씩 증가한다.
  4. setCount가 실행되면 다시 렌더링이 실행된다.
  5. 렌더링이 실행되면 Counter가 다시 실행될 것이다.
  6. Counter 컴포넌트가 다시 실행되어도 count의 값은 초기화되지 않고 유지된다.

이렇게 진행이 되는데, 6번 컴포넌트 함수가 다시 실행 되더라도 count의 값이 초기화되지 않고 유지된다 는 어떻게 가능할까?

count의 값이 어떻게 초기화되지 않고 유지 되는지를 중심으로 차근차근 useState 에 대해 분석해보자.


아래는 Counter 컴포넌트를 렌더링 해주는 코드이다.

/** Javascript **/
function useState (initState) { }

function Counter () {
  const [count, setCount] = useState(1);

  window.increment = () => setCount(count + 1);

  return `
    <div>
      <strong>count: ${count} </strong>
      <button>증가</button>
    </div>
  `;
}

function render () {
	const $app = document.querySelector('#app');
	$app.innerHTML = Counter();
}

render();

useState를 실행하면 첫 번째 인자는 state를 반환하고, 두 번째 인자는 state를 변경하는 setState를 반환하다. 그리고 setState 를 실행하면 render가 실행된다.


그래서 대략 다음과 같은 형태의 코드가 될 것이다.

function useState(initState) {
  let state = initState; // state를 정의한다.
  const setState = (newState) => {
    state = newState; // 새로운 state를 할당한다
    render(); // render를 실행한다.
  }
  return [ state, setState ];
}

useState를 실행하면 내부에 state를 정의하고, setState를 실행하면 내부에 선언된 state를 변경할 것이다. 즉, 함수가 실행될 때 마다 결국 state의 값은 initState초기화 될 것이다.


예를 들어 다음과 같을 때

const [count, setCount] = useState(1); // state에는 항상 1이 들어간다.

state에는 항상 1이 들어간다.


그래서 state의 값은 내부가 아닌 외부에서 관리해야 한다.

let state = undefined;
function useState(initState) {
  // state에 값이 없을 때만 초기화를 진행한다.
  if (state === undefined) {}
    state = initState;
  }
  const setState = (newState) => {
    state = newState; // 새로운 state를 할당한다
    render(); // render를 실행한다.
  }
  return [ state, setState ];
}

function Counter() { /*생략*/ }
function render () { /*생략*/ }

render();

렌더링 할 때 마다 초기화되지 않고 잘 작동한다.


그렇다면 만약 useState와 Component가 여러 개라면 어떨까?

function Counter () {
  const [count, setCount] = useState(1);

  window.increment = () => setCount(count + 1);

  return `
    <div>
      <strong>count: ${count} </strong>
      <button>증가</button>
    </div>
  `;
}

function Cat () {
  const [cat, setCat] = useState('고양이');

  window.meow = () => setCat(cat + ' 야옹!');

  return `
    <div>
      <strong>${cat}</strong>
      <button>고양이의 울음소리</button>
    </div>
  `;
}

function render () {
  app.innerHTML = `
    <div>
      ${Counter()}
      ${Cat()}
    </div>
  `;
}

한 개의 state 변수로 두 개의 state를 관리하기 때문에 count와 cat이 똑같은 값을 보여주게 된다.


이를 해결하기 위해서 외부의 state 갯수useState가 실행되는 횟수만큼 만들어주면 될 것이다.

let currentStateKey = 0; // useState가 실행 된 횟수
const states = []; // state를 보관할 배열
function useState(initState) {
  // initState로 초기값 설정
  if (states.length === currentStateKey) {
    states.push(initState);
  }

  // state 할당
  const state = states[currentStateKey];
  const setState = (newState) => {
    // state를 직접 수정하는 것이 아닌, states 내부의 값을 수정
    states[currentStateKey] = newState;
    render();
  }
  currentStateKey += 1;
  return [ state, setState ];
}

function Counter () { /*생략*/ }
function Cat () { /*생략*/ }

const render = () => {
  app.innerHTML = `
    <div>
      ${Counter()}
      ${Cat()}
    </div>
  `;
  // 이 시점에 currentStateKey는 2가 될 것이다.
  // 그래서 다시 0부터 접근할 수 있도록 값을 초기화 해야 한다.
  currentStateKey = 0;
}

여기까지가 useState의 핵심원리 라고 할 수 있다.


이렇게 정리하면서 느낀것은 useState의 원리가 클로저와 비슷하다는 것이다. 직접적으로 클로저를 사용하진 않았지만 유사한 개념이라고 볼 수 있다.

function 클로저() {
  let 죽지않는_변수 = 0;
  return function () {
    죽지않는_변수 += 1;
    console.log('죽지않는_변수 : ' + 죽지않는_변수);
  }
}
const 클로저로_만들어진_함수 = 클로저();
클로저로_만들어진_함수(); // 죽지않는_변수 : 1
클로저로_만들어진_함수(); // 죽지않는_변수 : 2
클로저로_만들어진_함수(); // 죽지않는_변수 : 3
클로저로_만들어진_함수(); // 죽지않는_변수 : 4

클로저의 개념처럼 useState 함수의 바깥에서 state관리하기 때문에 state값이 유지되는 것이다.


useState에 대해 정리하면서 자료를 찾아보다가 정말 좋은 글이 있어서 글을 따라 동작원리를 분석해보았다. 당연하게 생각했던 부분을 뜯어보니 당연하지 않고 다 이유가 있다.
그리고 클로저의 원리를 이렇게 가까운 곳에서 사용하고 있었다니,,, 괜히 클로저 클로저 하는게 아니다.

profile
효율에 미친자

0개의 댓글