React - useState Hook

이소라·2022년 9월 7일
0

React

목록 보기
14/23

useState Hook

  • useState Hook을 사용하여 함수 컴포넌트에서 state를 사용할 수 있음
    • useState를 호출하여 state 변수를 선언할 수 있음
    • 일반적으로 함수 스코프의 변수는 함수가 끝났을 때 사라지지만, state 변수는 React에 의해 유지됨
import React, { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);
  • useState는 state의 초기 값를 인수로 전달 받음

    • 함수 컴포넌트의 state는 클래스 컴포넌트의 state와 달리 객체일 필요 없음
    • state 값으로 숫자, 문자열 타입을 가질 수 있음
  • useState는 state 변수와 이 변수를 갱신시킬 수 있는 함수 setState를 반환함

    • 변수의 값을 갱신하려면 setState 함수를 호출해야 함
    • 배열 구조 분해 할당을 통해 state 변수와 setState 함수의 이름을 변경할 수 있음
  • React는 리렌더링할 때 state 변수를 기억하고, 가장 최신에 갱신된 값을 제공함

    • 리렌더링할 때 setState 함수는 변하지 않으므로, useEffectuseCallback의 dependency 목록에서 setState가 생략됨
  • 컴포넌트 내에서 여러 state 변수를 사용할 수 있음

function ExampleWithManyStates() {
  // 여러 개의 state를 선언할 수 있습니다!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  ...
}

Functional updates

  • 새 state가 이전 state를 사용하여 계산되는 경우, useState의 두 번째 인자 setState에 값 대신 함수를 전달할 수 있음
    • 이 전달되는 함수는 이전 state 값을 인수로 받아서, 갱신된 state 값을 반환함
function Counter({initialCount}){
  const [count, setCount] = useState(initialCount);
  return (
    <>
    Count: {count}
      {/* button : normal form*/}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      {/* button : functional form*/}
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

Note

  • useState는 class 컴포넌트의 setState 메서드와 달리 객체 상태를 자동적으로 합쳐주지 않음
    • 해결 방법 1 : spread 문법을 사용하여 객체를 합쳐주어야 함
    • 해결 방법 2 : useReducer를 사용함
const [state, setState] = useState({});
setState(prevState => {
  // Object.assign을 대신 사용해도 됨
  return {...prevState, ...updatedValues};
});

Lazy initial state

  • initialState 인수는 첫 렌더링 때만 사용되고, 그 이후 렌더링에서는 무시됨
  • 초기 상태가 비싼 연산의 결과일 경우, initialState에 값 대신 함수를 전달할 수도 있음
    • 이 전달받은 함수는 첫 렌더링에서만 실행됨
const [state, setState] = useState(() => {
  const initialState = someExpansiveComputation(props);
  return initialState;
});

bailing out of a state update

  • state Hook을 현재 state과 동일한 값으로 갱신할 경우, React는 자식을 렌더링하거나 effect를 실행하지 않고 회피함(bail out)
    • React는 Object.is 비교 알고리즘을 사용하여 이전state와 새 state를 비교함
    • 아래와 같은 경우, 두 state를 같다고 여김
      • 두 값이 모두 undefined이거나 null인 경우
      • 두 값이 모두 true이거나 false인 경우
      • 두 값이 모두 같은 글자, 길이, 순서를 가진 string인 경우
      • 두 값이 모두 같은 값을 가진 umber이거나 NaN인 경우
      • 두 값이 모두 한 메모리 주소를 가리키는 object인 경우
    • 실행을 회피하기 전에 React에서 특정 컴포넌트를 리렌더링하는 것이 여런히 필요할 수도 있음
      • 렌더링 시 비싼 연산을 하는 경우, useMemo를 사용하여 최적화할 수 있음

Batching of state updates

  • React 18부터 성능을 향상시키기 위해 리렌더링을 한 번 할 때 여러 state 갱신을 그룹화할 수 있음
    • React 18 이전에는 React event handler 안의 업데이트만 그룹화(batch)할 수 있었음
      • promises, setTimeout, native event handler나 다른 이벤트들은 React 안에서 기본적으로 그룹화되지 않았음
// Before React 18
// batching
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end
}

// no batching
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will render twice, once for each state update
}, 1000);
  • React 18에서부터 createRoot를 사용하면서, 업데이트들이 어디서 기원되었는지 상관없이 모든 업데이트들은 자동적으로 그룹화(batch)됨
    • 이는 promises, setTimeout, native event handler나 다른 이벤트 안에서의 업데이트들도 React 이벤트 안에서의 업데이트와 같은 방법으로 그룹화된다는 것을 의미함
// After React 18
// batching
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end
}

// batching
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end
}, 1000);
  • 자동 업데이트 그룹화(batching)의 선택적 이탈을 위해서 flushSync를 사용할 수 있음
import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCount(c => c + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setFlag(f => !f);
  });
  // React has updated the DOM by now
}



useState Hook의 동작 원리

Closure를 사용한 간략한 useState 구현

  • useState Hook을 작게 구현하기 위해서 closure 개념을 사용함
    • closure : 함수가 lexical scope밖에서 실행되더라도, 함수는 함수가 정의된 lexical scope을 기억하고 접근할 수 있음
function useState(initialValue) {
  let _val = initialValue; // _val은 useState에 의해 생성된 지역 변수
  
  function state() {
    // state는 inner function으로 closure임
    return _val;
  }
  
  function setState(newVal) {
    _val = newVal; // _val을 노출시키지 않고 _val의 값을 설정함
  }
  
  return [state, setState]; // 외부에서 사용하기 위해서 state와 setState를 노출시킴
}

let [foo, setFoo] = useState(0);
console.log(foo()); // 0이 출렸됨 - initialValue
setFoo(1); // useState 함수의 lexical scope 안에 존재하는 지역변수 _val의 값을 1로 설정함
console.log(foo()); // 1이 출력됨 - newVal
  • 위 코드의 useState hook에서 2개의 inner function이 존재함
    • state : 위에서 정의한 지역 변수 _val을 반환하는 함수
    • setState : 지역 변수 _val을 전달받은 매개변수 newVal로 설정하는함수
    • statesetState는 내부 변수인 _val에 접근하고 조작할 수 있음
      • statesetStateuseState의 스코프에 대한 접근을 유지함
      • 이러한 참조를 closure라고 부름

Function Components에서의 간단한 예시

function Counter() {
  const [count, setCount] = useState(0);
  
  return {
    click: () => setCount(count() + 1),
    render: () => console.log('render:', { count: count() })
  };
}

const C = Counter();
C.render(); // render: { count: 0 }
C.click();
C.render(); // render: { count: 1 }
  • 위 코드에서는 DOM을 렌더링하는 대신에, 상태를 console.log로 출력했음

Stale Closure

  • 위에서 구현한 useState를 실제 React API에 맞게 변형하려면, 상태(_val) 타입이 함수가 아닌 변수이어야 함
    • 그러나 _val을 함수로 감싸지 않고 그대로 노출시키면, 에러가 발생됨
function useState(initialValue) {
  var _val = initialValue;
  
  function setState(newVal) {
    _val = newVal
  }
  return [_val, setState]; // _val을 그대로 노출시킴
}
var [foo, setFoo] = useState(0)
console.log(foo) // 0이 출력됨
setFoo(1) // useState의 scope 안의 _val를 1로 설정함
console.log(foo) // 0이 출력됨
  • 위 코드는 Stale Closure 문제 중 하나의 형태임
    • useState의 반환값으로부터 foo를 구조 분해 할당했을 때, foo는 초기에 useState를 호출했을 때의 _val를 참조하고 이 값은 바뀌지 않음

Closure in Modules

  • 위에서의 Stale Closure 문제를 해결하기 위해서, closure를 또 다른 closure 안으로 옮김
const MyReact = (function() {
  let _val; // module scope에 _val을 둠
  
  return {
    render(Component) {
      const Comp = Component();
      Comp.render();
      return Comp;
    },
    useState(initialValue) {
      _val = _val || initialValue; // useState를 호출할 때마다 _val에 값이 할당됨
      function setState(neVal) {
        _val = newVal;
      }
      return [_val, setState];
    }
  }
})();
  • 위 코드에서 Module pattern을 사용하여 React를 간략하게 구현함
    • React와 비슷하게, 컴포넌트에 대한 상태를 추적함
    • 이 구조는 MyReact가 인수로 받은 컴포넌트를 렌더링할 수 있고, 매번 내부 변수인 _val에 값을 할당할 수 있게 함
function Counter() {
  const [count, setCount] = MyReact.useState(0);
  
  return {
    click: () => setCount(count + 1),
    render: () => console.log('render:', { count });
  };
}

let App;
App = MyReact.render(Counter); // render: { count: 0 }
App.click();
App = MyReact.render(Counter); // render: { count: 1 }

참고

0개의 댓글