manage state data using setState()

zzwwoonn·2022년 5월 10일
0

React

목록 보기
12/23

리액트 16.8 이전 버전에서는 함수형 컴포넌트에서는 상태를 관리할 수 없었다. 그런 이유 때문인지 책으로 공부를 하거나 공식 문서로 공부를 할 때 전부 클래스형 컴포넌트로 설명이 되어있다. 어쩔 수 없이 클래스형 컴포넌트로 개념을 이해하고 함수형으로 바꿔서 실습을 진행하고 있다. (구글에는 함수형 컴포넌트 관련 자료가 많이 나와있어 그나마 다행이다)

리액트 16.8 에서 Hooks 라는 기능이 도입되면서 함수형 컴포넌트에서도 상태를 관리할 수 있게 되었다. state는 useState 라는 함수를 통해서만 값의 변경이 이루어 지는데, 이게 바로 리액트의 Hooks 중 하나이다.

useState를 이용하지 않고서 state를 직접적으로 고치는 건 절대 불가


import { useState } from 'react';
import React from 'react';

function Counter() {
    const [number, setNumber] = useState(0);

    const increase = () =>  {
        setNumber(number + 1);
    }

    const decrease = () => {
        setNumber(number - 1);
    }
    
    return (
        <div>
            <h1>{number}</h1>
            <button onClick={increase}>+1</button>
            <button onClick={decrease}>-1</button>
        </div>
export default Counter;

< 실행 결과 >

동영상으로 올리고 싶지만 어떻게 하는지 모르겠다. 아무튼 잘 동작한다.

지금은 Setter 함수(setState)를 사용 할 때 업데이트 하고 싶은 새로운 값을 파라미터로 넣어주고 있는데 그 대신에 기존 값을 어떻게 업데이트 할 지에 대한 함수(아래 코드)를 등록하는 방식으로도 값을 업데이트 할 수 있다.

setNumber(prevNumber => prevNumber + 1);

이 때 문제점이 발생한다. ( 문제점? 짚고 넘어갈 점!! 심지어 아주 중요 )

관련 문서를 보면서 공부를 하고 있었는데 밑에 댓글에 다음과 같은 내용이 있었다.

setNumber(number + 1); 와 setNumber(prevNumber => prevNumber + 1); 에 차이점을 알고 싶습니다. 둘 중 아무거나 사용해도 무방한 것인가요?

믿기지 않겠지만 나도 똑같은 생각을 했다.
"둘이 똑같으면 걍 암거나 쓰면 되나? 분명히 뭔가 다를거 같은데..."

그래서 다시 관련 공부를 했고, 완벽하게 이해를 했다. 이게 뭐라고 되게 뿌듯하다. 그에 대한 과정까지 기록해두려 한다. 개인적으로는 이 내용이 useState 에 대한 기본적인 이해보다 훨씬 더 중요한? 개념이라고 생각한다.

import { useState } from 'react';
import React from 'react';

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

    const onClick1 = () => {
        setCount(count+ 1);
        setCount(count+ 1);
        console.dir(count);
    }

    const onClick2 = () => {
        setCount(count=> count+ 1);
        setCount(count=> count+ 1);
        console.dir(count);
    }
    
    return (
        <div>
            <h1>{count}</h1>
            <button onClick={onClick1}> count + 1 버튼 </button>
            <button onClick={onClick2}> count = count + 1 버튼 </button>
        </div>
    );
}

export default Counter;

위 코드의 결과는 아래와 같다.

동작을 예상해보자.

Setter 함수(setState)를 사용 할 때 넘겨주는 파라미터로

  1. 업데이트 하고 싶은 새로운 값을 넣는 방식
  2. 기존 값을 어떻게 업데이트 할 지에 대한 함수를 등록하는 방식

이렇게 2가지 방법을 사용할 수 있다고 했다. 만약 이 두가지 방법이 똑같이 동작한다면 왼쪽 버튼과 오른쪽 버튼의 결과가 똑같이 나올 것이다. (아마도 2씩 증가할 것이다.)

하지만 왼쪽 버튼은 1씩 증가하는 반면, 오른쪽 버튼은 2씩 증가한다. 왜 그럴까?

state가 여럿일 경우에 다른 state의 변화로 리렌더링이 거의 동시에 이루어지거나 하게 되면 useState 왼쪽의 state가 이전의 값임을 보장할 수 없을 때가 있다.

위의 코드에서

setCount(count+ 1); // 1-1
setCount(count+ 1); // 1-2

setCount(count=> count+ 1); // 2-1
setCount(count=> count+ 1); // 2-2

1-1에서의 결과값이 1-2의 count로 들어간다는 보장 , 2-1의 결과값이 2-2의 왼쪽 count로 들어간다는 보장이 없다는 의미이다.

이는 바로 state의 변화가 비동기적(async)으로 일어나기 때문이다.

setState가 호출되면 바로 state가 바뀌는 게 아니라 리액트에게 변경해야할 state를 알려주는 것이고 리액트는 다른 state의 변경 요청까지를 다 받은 다음 한 번에 묶어서 state를 변경하여 리렌더링시킨다 (promise나 setTimeout 등을 제외하고 그 안에 실행된 모든 state들을 하나로 묶는다)
=> React batch update

state 값을 변경하면 해당 컴포넌트와 그 자식 컴포넌트들은 리렌더링이 일어난다.

< 리렌더링이 일어나는 조건 >

  • state 변경이 있을 때
  • 새로운 props이 들어올 때
  • 부모 컴포넌트가 렌더링 될 때
  • shouldComponentUpdate에서 true가 반환될 때
  • forceUpdate가 실행될 때

그렇다면 state가 여러개 있고, 이것들을 한번에 업데이트하고 싶다면 여러번 리렌더링이 일어나지 않을까?

정확히 그렇다. 여러번 불필요한 리렌더링이 많이 일어나게 되어서 어플리케이션의 성능에 영향을 미칠 수 있다. 리액트 공식 문서에 따르면 이를 해결하기 위해 나온게 React batch updating 이라고 한다.

React batch update
state를 각각 업데이트하는 대신 batch update (일괄 업데이트)를 해 컴포넌트의 렌더링 횟수를 최소화할 수 있다. React batch update란 리액트 성능을 위해 setState를 단일 업데이트 (batch update)로 한번에 처리하는 것이다. 현재는 (react 16 및 이전 버전) 리액트 핸들러의 업데이트에만 기본적으로 batch update가 적용된다.

기능의 주된 아이디어는 리액트의 이벤트 핸들러와 life cycle method를 사용하여 얼마난 많은 setState를 호출하든 간에 단 하나의 업데이트로 일괄 처리를하며, 리렌더를 한 번만 발생하게 하는 것이다.

정리를 해보자면 setState()는 비동기식 함수이며 동일한 주기 동안 여러 호출을 일괄 처리할 수 있다 => 배치

또 다른 예시로 동일한 주기에서 항목 수량을 두 번 이상 늘리려고 하면 setState() 호출을 여러 번 하게 되는데 후속 호출은 동일한 주기의 이전 호출 값을 무시하므로 수량은 한 번만 증가하는 것이다.

위의 코드에서 1을 더하는 것과 같이 state의 업데이트가 이전의 값에 의존하는 경우 반드시 setState 함수의 인자에 함수를 넣어야 한다. 그래야만 함수에서 받는 인자(count)가 이전의 값임을 장담할 수 있다. 이전의 값에 의존할 필요가 없는 경우에는 쓸데없이 코드가 길어지니 불필요하다.


[ Functional setState is the future of React ] 이라는 원서에서 위의 개념에 대한 설명을 매우 깔끔하게 잘 해놨더라.

그래서 복습? 개념으로 원서의 내용을 번역하여 인용, 여기다 다시 정리 해본다.

setState()가 호출 될 때 어떤 일이 일어나는지 생각해보자.

리액트는 먼저 setState()에 전달한 객체를 현재 상태로 합친다. 그러면 reconciliation을 시작한다.

Reconciliation(재조정)
리액트는 컴포넌트에서 prop이나 state가 변경될 때, 직전에 렌더링된 요소(element)와 새로 반환된 요소를 비교(비교 알고리즘 - Diffing Algorithm)하여 실제 DOM을 업데이트 할지 말지 결정해야 한다. 이때 두 element가 일치하지 않으면 리액트는 새로운 요소로 DOM을 업데이트 하는데, 이러한 프로세스를 reconciliation이라고 한다.

새로운 리액트 엘리먼트 트리 (UI의 객체 표현)를 만들고, 새 트리를 이전(old) 트리와 비교하고, setState()에 전달한 객체를 기반으로 변경된 부분을 파악한 다음 DOM을 최종적으로 업데이트한다.

리액트는 여러 setState() 호출을 성능 향상을 위해 단일 배치 방식으로 작업을 한다.

이게 무슨 뜻일까?

첫째, “여러 setState() 호출”은 setState()를 한 번 이상 단일 함수 내에서 호출하는 것을 의미 할 수 있다:

state = {score : 0};
// multiple setState() calls
increaseScoreBy3 () {
 this.setState({score : this.state.score + 1});
 this.setState({score : this.state.score + 1});
 this.setState({score : this.state.score + 1});
}

이제 리액트가 “상태 세팅(set-state)“을 세 번이나 하는 대신 “여러 setState() 호출”을 만났을 때 리액트가 위에서 설명한 많은 양의 작업을 피하고 현명하게 이렇게 말하는 것과 같다

“아니! 나는 이 산을 세 번 오르지 않고 매 상태의 일부를 옮겨서 업데이트 할 거야. 아니? 차라리 컨테이너를 가져다가 모든 조각을 모아서 한 번만 업데이트 하겠어. 딱 한번만!”

이 말인 즉슨, 배치(batch)로 처리 중이라는 것이다!

setState()에 전달하는 것은 일반 객체라는 것을 기억하자. 이제는 리액트가 “여러 setState() 호출”을 만나면 각 setState() 호출에 전달 된 모든 객체를 추출하여 배치 작업을 수행하고 이를 머지(merge)하여 단일 객체를 형성 한 다음 해당 단일 객체를 사용하여 setState()를 수행한다.

자바스크립트의 객체를 머지(merging)하는 것은 다음과 같을 수 있다.

const singleObject = Object.assign(
{},
objectFromSetState1,
objectFromSetState2,
objectFromSetState3
);

이런 패턴을 오브젝트 컴포지션(object composition)이라고한다.

자바스크립트에서 만약 3개의 객체가 동일한 키를 갖고 있다면, Object.assign()에 마지막으로 전달 된 객체의 키 값이 우선된다.

예제

const me  = {name : "Justice"}, 
      you = {name : "Your name"},
      we  = Object.assign({}, me, you);
we.name === "Your name"; //true

console.log(we); // {name : "Your name"}

you가 we에 머지된(merged) 마지막 객체이기 때문에, you의 객체에 있는 name의 값 ”Your name“이 me 객체에 있는 name의 값을 덮어 쓴다. 그래서 ”Your name“이 we의 객체가 된다.

따라서, setState()에서 호출 할 때마다 매번 객체를 전달하고 리액트가 객체를 머지(merge) 하게된다.

즉, 우리가 전달한 여러 객체 중에서 새로운 객체를 구성한다. 객체 중 하나에 동일한 키가 포함되어 있으면 동일한 키가 있는 마지막 객체의 키 값이 저장되는 것이다.

setState()에 객체를 전달하는 것이 문제가 아니다. 진짜 문제는 이전 상태에서 다음 상태를 계산할 때 객체를 setState()로 전달하는 것에 있다. 이것은 절대 안전하지 않다.

0개의 댓글