Frontend Study - 1/React

React : state, useState에 대하여, snapshot, batches

갓데미 2023. 3. 1. 00:15

목차 

1. React state란? state의 필요성

2. useState

  1) setState

    * React batches state updates 

  2) initial state

  3) 오브젝트와 배열의 업데이트

 

 

1. state란? state의 필요성

다음페이지를 눌렀을 때 기존 페이지에서 다음 페이지로 이동하기는 것, 장바구니에 담기를 클릭하였을 때 물건이 추가로 장바구니에 담기는 것과 같이 유저 상호작용 결과로 즉각적인 UI 변경이 필요한 경우가 있다. 이것들이 구현되기 위해 컴포넌트들은 우선적으로 기존의 값들을 기억해야 한다. 기존값이 1이라고 했을 때 +1을 하고 화면에 2를 보여주기 위해서는 기존값 '1'에 대한 기억이 필요하다는 것. 리액트에서는 이러한 기억이 필요한 컴포넌트 별 메모리를 가리켜 'state'라고 한다. 

 

위에 말한 예시를 구현하기 위해서 기존에 우리가 알던 일반 '변수'로는 충분하지 않다. 값을 +1해서 업데이트 해주어도 UI에 2로 바로 변경 값이 표기되지 않기 때문이다. 여기에는 두가지 이유가 있다. 

 

1) 로컬 변수가 렌더링 전 후로 유지되지 않는다. React가 컴포넌트를 다시 렌더링할 때, 처음부터 다시 렌더링하기 때문에 로컬 변수는 다시 처음에 할당 되었던 초기값이 되어버리는 것.  

 

2) 로컬 변수의 변경은 렌더링을 발생시키지 않는다. React는 일반 변수를 변경했다고 해서 컴포넌트를 리렌더링의 필요성을 인식하지 않는다. 일반변수에 +1을 하여 해당 변수값이 변경되었다 하더라도 렌더링이 되지 않았으니 화면에는 기존 값이 남아있게 된다.

 

즉, 새로운 데이터로 컴포넌트를 업데이트 하고 이것을 화면에 적용시키기 위해서는, 데이터의 기존값을 렌더링 사이에 저장해두어야 하고, 새로운 데이터로 업데이트됨에 따라 리렌더링이 발생해야 한다.

 

이것을 위해 우리는 리액트에서 state를 사용할 수 있다. 

 

2. useState

useState는 리액트의 'Hook'의 일종이다. 

 

hook은 'use'로 시작되며 컴포넌트나 직접 만든 훅 내에서의 최상위 레벨에서 호출할 수 있다. 조건문, 반복문, 중첩 함수 내에서는 훅을 사용할 수 없다. 리액트가 렌더링될 때만 사용할 수 있는 특수한 함수이다.

 

useState는 여러 Hook들 중 state, 즉 로컬 상태값 생성 및 관리에 관여하는 훅이다. 

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

구조분해 할당을 통해 현재 상태 값인 state, 그 state의 값을 변경해주기 위한 setState 함수로 나뉜다.

 

 

1) setState

 

state 값은 일반 변수처럼 변경할 수 없다. state = state+1 이런식으로 직접적인 값의 변경이 불가능하고 setState함수를 통해 변경 할 수 있다.

 

위 코드를 예로들면 state이라는 상태값을 변경하기 위해 setState(x)의 x자리에 변경할 값을 직접적으로 넣어줄 수 있다. 또 다른 방법으로 x자리에 updater function을 넣어 과거의 값과 비교하여 다음 들어갈 상태값을 업데이트 해줄 수 도 있다.

 

이전의 상태 값을 참고하여 다음 값을 정하려 할 때,

단순히 원래의 상태 값을 그대로 가져와서 사용하는 방식은 제대로 동작하지 않는다. 

 

const [age, setAge] = useState(42)

function handleClick() {
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
}

 

위의 예시를 보면 기존 age를 42로 가정하였을 때, 위 함수를 통한 결과값은 45가 아니라 43이다. set함수를 호출하는 것은 이미 실행중인 코드의 age 상태값을 업데이트 하지 않기 때문이다. state는 '스냅샷' 처럼 행동한다. setAge(age+1)의 age값은 함수가 실행되는 순간의 값으로 지정되게 된다. 

 

 

// setSomething(something => something + 1);

function handleClick() {
  setAge(a => a + 1); // setAge(42 => 43)
  setAge(a => a + 1); // setAge(43 => 44)
  setAge(a => a + 1); // setAge(44 => 45)
}

위의 예시처럼 set함수내에 이전 상태값을 인자로 받는 updater function을 지정해 주면 제대로 동작한다. updater function은 순수 함수여야 하며 하나의 인자로 과거 상태를 받고 변화할 상태 값을 리턴해야 한다. 

 

 

이 때 리액트는 상태 업데이트를 처리하기 전, 이벤트 핸들러에 있는 모든 코드가 실행될 때까지 기다린다. 

 

* React batches state updates 리액트 상태 일괄처리

리액트는 상태 업데이트를 처리하기 전에 이벤트 핸들러 내의 모든 코드가 실행될 때 까지 기다린다. 위의 코드에서 setAge(age + 1) 가 3번 모두 실행된 이후에 최종적으로 상태를 업데이트 하는 것이다. 

 

레스토랑에서 주문을 받는 웨이터를 예로 들면 이해가 쉽다. 웨이터가 주문을 받을 때는 첫번째 요리에 대한 주문만 받고 바로 주방에 알려주지 않는다. 모든 요리에 대한 주문을 받고, 기존 주문에 수정사항이 있으면 적용하고, 심지어 다른 테이블의 주문까지 종합한 뒤에 주방에 알려주게 된다. 

 

이 처럼 리액트 또한 이벤트 핸들러 내의 모든 코드를 종합한 이후, 최종 상태 값을 업데이트 하고 화면을 업데이트 하는 것. 만약 변경하고자 하는 값이 기존의 값과 중복되는 부분이 있다면 Object.is 비교에 의해 리액트는 해당 컴포넌트와 자식들에 대한 리렌더링을 스킵하는 최적화 작업을 진행한다. 이렇게 함으로써 하나의 이벤트 동안 발생할 수 있는 여러 번의 리렌더링을 방지할 수 있다. 

 

그렇다고 여러번의 의도적인 이벤트들(ex) 클릭 이벤트)에 대해서도 모두 batching하는 것은 아니다. 각각의 이벤트는 독립적으로 처리된다. 클릭을 여러번 한다고 해서 그 클릭들을 모두 종합한 최종 결과만 반영하는 것이 아니라는 것. React는 batching을 수행하는 것이 안전한 경우에만 수행한다. 예컨데 두번 클릭하였는데 첫번째 클릭이 버튼을 비활성화 시키는 경우 두번째 클릭이 다시 제출되지 않도록 보장한다.

 

 

 

2) InitialState

 

initialState는 state값에 들어갈 초기값을 의미한다. 만약 initialState에 함수를 넣었다면, 그 함수는 initializer 함수로 취급된다. 순수함수여야 하고 인자를 가지면 안되며 특정 값을 필수로 리턴해야한다. 리액트는 컴포넌트가 initializing될 때 해당 함수를 실행할 것이고 거기서 리턴된 값을 initialState에 값을 넣어 줄 것.

function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos());

initialState에 들어갈 값을 위와 같이 함수 실행 "()" 과 같이 설정해 준다면, 모든 렌더링에서 이 initializer 함수를 실행하게 된다. todo앱을 예시로 인풋 하나하나를 입력하며 렌더링이 될 때마다 해당함수가 실행되게 되는 것다. 

 

function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos);

위와 같이 구현해야 한다. 함수를 보내주는 것이지, 함수 실행의 결과를 보내주는 것이 아니다. 함수를 보내주면 리액트는 오직 initialization때에만 해당 함수를 실행할 것이다. 

 

 

Strict 모드에서 리액트는 initializer function이 순수함수인지 체크하기 위해 두 번 호출한다. 개발자 모드에서만 그렇게 동작하니 걱정하지 않아도 된다.

const [index, setIndex] = useState(0);

 

위의 사례로 종합해보면, 우선 index의 초기값은 0으로 설정되고, setIndex를 통해서 index의 값을 변경할 수 있다. setIndex를 통해 값을 업데이트 하게 되면 리액트는 컴포넌트를 리렌더링 시킨다. 이 때 useState훅을 통해 index의 값은 저장되고 기억된다.

즉, setIndex를 통해 index의 값을 +1을 시키면 컴포넌트가 다시 렌더링 되면서 index의 값이 초기값이었던 0으로 재할당 되는 것이 아니라, +1된 값을 기억한다는 것. 

 

 

+ 함수자체 저장하기

const [fn, setFn] = useState(someFunction);

function handleClick() {
  setFn(someOtherFunction);
}

initialStated와 set함수내의 함수가 들어있음으로 리액트는 각각 someFunction을 Initializer function / someOtherFunction을 updator function으로 가정하고 호출할 것이다. 그래서 이 코드는 호출에 대한 결과값을 저장한다. 그런데 이 때 만약 결과값이 아닌 함수 자체를 저장하려면 아래와 같이 해주면 된다.

 

const [fn, setFn] = useState(() => someFunction);

function handleClick() {
  setFn(() => someOtherFunction);
}

 

 

3) 오브젝트와 배열의 업데이트

 

state에 오브젝트와 배열을 넣을 수 있다. 다만 React에서 state는 읽기 전용으로 간주되기 때문에 기존 객체를 직접 변경하는 대신 교체 하는 방식으로 수정해야 한다. 

// 🚩 Don't mutate an object in state like this:
form.firstName = 'Taylor';

// ✅ Replace state with a new object
setForm({
  ...form,
  firstName: 'Taylor'
});

 

이는 리액트가 렌더링 프로세스를 최적화하는 데에 '불변성' 개념을 사용하기 때문이다. 자바스크립트에서 객체나 배열이 수정되면 그 참조는 변경되지 않는다. 따라서 우리가 상태를 직접적으로 변이시키면 React는 상태가 변경되었다는 것을 감지하지 못하고, 새로운 상태를 반영하는 UI를 업데이트하지 못할 수 있다. 

 

이 문제를 피하기 위해서 객체나 배열의 복사본을 만들고 그 복사본을 수정한 다음 해당 복사본을 상태값에 대체하는 것이다.

 

 

 

 

참고사이트

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

https://beta.reactjs.org/reference/react/useState#updating-state-based-on-the-previous-state

https://beta.reactjs.org/learn/state-a-components-memory