[React] State

state가 필요한 이유

함수 컴포넌트는 함수이다.

함수 컴포넌트에서 내부적으로 상태를 관리해야 하는 일이 필요하다.

 

아래 예시 코드를 보자

import React from 'react';

function Counter() {
    let count = 0;
  return (
    <div>
        <button onClick={() => (count += 1)}>+1</button>
        <br/>
      Counter: {count}
    </div>
  );
}

button을 클릭할 때 count 변수의 값이 증가하고 출력하는 컴포넌트이다.

하지만 button을 클릭해도 증가하는 count 변수의 값이 출력되지 않는다.

(초기값 0으로 그대로 변하지 않음)

= 컴포넌트가 초기에 렌더링을 실행하고 변수의 값이 바뀐 뒤로 렌더링이 되지 않는다.

 

➡ state가 필요한 이유!

 

useState() hook

컴포넌트가 상태를 관리한다는 건 그 상태값을 변경할 수 있고,

변경되면 그 컴포넌트가 렌더링을 일어날 수 있게 한다.

 

이를 위해서는 useState() 함수가 필요하다.

useState()를 사용하기 위해서는 import를 해야 한다.

import React, {useState} from "react";

 

useState()

두 가지의 값(state, setState)이 들어있는 배열을 return 한다.

state : 값 / setState : 값을 변경할 때 쓰는 함수

 

state가 아닌 다른 이름으로 사용할 수 있다.

ex) count 값을 관리한다? 

count [count, setCount] = useState();

 

useState 함수를 호출 할 때 초기값을 넘길 수 있다.

0으로 시작하고 싶다면 useState(0); 으로 호출하면 된다.

 

setState() 함수는 State 값을 변경할 수 있다.

<button onClick={() => setCount(count + 1)}>+1</button>

setState()는 컴포넌트의 re-rendering을 발생시킨다.

 

state 불변성

불변셩 (immutable)

"변하지 않는 성질"

프로그래밍에서 불변성을 지킨다 ➡ 메모리 영역의 값을 직접적으로 변경하지 않는다.

 

setState() ➡ 컴포넌트 re-rendering

이 과정에서,

불변성을 지키지 않고, 메모리 영역의 값을 직접 변경하면 리액트는 state가 바뀌었다고 인지하지 못한다.

 

왜냐하면 리액트는 이전 state과 이후 state를 비교할 때,

얕은 비교(Shallow Compare)을 하기 때문이다!

 

 데이터 타입을 통해 살펴보자

 

원시 타입(Primitive type)

boolean, number, string

불변성을 가지고 있다.

변수에 원시 타입의 값을 할당하면, 메모리에 값 자체가 저장된다.

 

참조타입(Reference type)

Object

불변성을 가지고 있지 않다.

변수에 참조 타입의 값을 할당하면, 메모리 값이 담긴 주소가 저장된다.

 

원시 타입과 참조 타입을 setState() 할 때 신경써야 할 부분이 다르다.

원시 타입은 이미 불변성을 가지고 있기 때문에 따로 고려해야할 사항이 없지만,

참조 타입은 불변성을 가지고 있지 않기 때문에 불변성을 가지도록 호출을 해야 한다.

 

원시 타입 state 다루기

원시 타입(Primitive type) state 다루어보기 - boolean, number, string

 

 

+1 버튼 : 클릭하면 Counter 값을 1 증가 시킨다.

Show and Hide 버튼 : 클릭하면 Counter를 나타나거나 사라지게 한다.

Change Operator : +, -, * 중 하나를 랜덤하게 선택하여 Counter 계산에 적용한다.

 

    // 값, 값을 변경할 때 쓰는 함수
    const [count, setCount] = useState(0);
    const [show, setShow] = useState(true);

    // +, -, *
    const operators = ["+", "-", "*"];
    const [operator, setOperator] = useState(operators[0]);

 

참조 타입 state 다루기

위의 원시 타입 데이터들을 객체로 다루어보자.

const [info, setInfo] = useState({
    count: 0,
    show: true,
    operator: operators[0],
})

먼저 state값을 변경해보자.

count 변수를 예로 들자면 info.count로 변경하면 된다.

 

setState 함수를 변경해보자.

만약

info.show = !info.show;
const newInfo = info;
setInfo(newInfo);

이처럼 새로운 객체를 생성하여 그 값을 넣어주면 어떻게 될까?

state값이 변경되더라도 렌더링이 되지 않는다.

react가 state 값이 변경되었다고 인지하지 못하기 때문이다.

 

그렇다면 어떻게 setInfo를 사용해야 할까?

<button onClick={() => {
        setInfo({... info, show: !info.show})
      }}>Show and Hide</button>

객체를 펼치고, 변경할 키와 값을 변경하면 된다.

 

Array 객체 다루는 방법 

import React, {useState} from "react";

const [array, setArray] = useState(["a", "b", "c", "d"]);

 

< 잘못된 사용법 >

array[0] = "e";
const newArray = array;
setArray(newArray);

< 올바른 사용법 >

setArray([...array, newItem]);
setArray(array.filter(arr => {}));

 

props와 state 비교

props

부모 컴포넌트가 자식 컴포넌트에게 전달하는 값

값을 자신(자식 컴포넌트)이 변경할 수 없음

 

state

자신(컴포넌트)이 스스로 관리하는 상태 값

값을 자신이 변경할 수 있음

 

props & state 공통점

props를 통해 값을 내려 받거나, 자신이 관리하고 있는 state가 변경되면 컴포넌트 렌더링이 발생한다!

 

비동기로 이루어지는 setState

https://ko.reactjs.org/docs/state-and-lifecycle.html

 

State and Lifecycle – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

State 업데이트는 비동기적일 수도 있다.

React는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있다.

this.props와 this.state가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당 값에 의존해서는 안 된다.

예를 들어, 다음 코드는 카운터 업데이트에 실패할 수 있다.

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

 

제로베이스 예시

import React, { useState } from "react";

export default function App() {
  const [number, setNumber] = useState(1);

  const add = () => setNumber(number + 1);
  const subtract = () => setNumber(number - 1);
  const multiplyBy2 = () => setNumber(number * 2);
  const multiplyBy2AndAddBy1 = () => {
    multiplyBy2();
    add();
  };

  return (
    <div>
      <h1>Number : {number}</h1>
      <div>
        <button onClick={add}>+ 1</button>
        <button onClick={subtract}>- 1</button>
        <button onClick={multiplyBy2}>*2</button>
        <button onClick={multiplyBy2AndAddBy1}>*2 + 1</button>
      </div>
    </div>
  );
}

예시 코드 중, 

const multiplyBy2AndAddBy1 = () => {
    multiplyBy2();
    add();
  };

부분에서, multiplyBy2()함수는 실행이 되지 않고 add() 함수만 실행이 되는 것을 볼 수 있다. 

왜?

State 업데이트는 병합되지 때문이다!

setState()를 호출할 때 React는 제공한 객체를 현재 state로 병합한다.

 

우리의 state는 사실 내부적으로 객체이다.

 

multiplyBy2() 함수와 add() 함수를 호출하게 된다면,

Object.assign() : 여러 개의 객체를 병합하는 메서드

Object.assign({ number, number: number * 2, number : number + 1 });

초기값이 3이라고 했을 때,

number * 2 => 6이 된다.

그 다음 연산을 수행할 때 6에서 시작하는 것이 아닌 초기값인 3부터 시작하기 때문에 

결과값은 number + 1 => 4가 된다.

// 3, 6, 4

➡ add() 함수만 실행되는 것처럼 보인다.

 

이럴 땐 어떻게 해야 할까?

 

이를 수정하기 위해 객체보다는 함수를 인자로 사용하는 다른 형태의 setState()를 사용한다. 그 함수는 이전 state를 첫 번째 인자로 받아들일 것이고, 업데이트가 적용된 시점의 props를 두 번째 인자로 받아들일 것이다.

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

위에서는 화살표 함수를 사용했지만, 일반적인 함수에서도 정상적으로 작동한다.

// Correct
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

이렇게 한다고 해서 setState()가 동기적으로 실행되는 것은 아니다.

하지만 Object.assign 메서드에서 인자를 함수로 넘겨주게 되면 다음 병합 시에 업데이트 된 값으로 병합할 수 있다.

 

 

단방향 데이터 흐름

데이터는 아래로 흐릅니다.

컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있다.

<FormattedDate date={this.state.date} />

FormattedDate 컴포넌트는 date를 자신의 props로 받을 것이고 이것이 Clock의 state로부터 왔는지, Clock의 props에서 왔는지, 수동으로 입력한 것인지 알지 못한다.

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

매개변수로 props를 받을 뿐이기 때문이다.

 

일반적으로 이를 "하향식(top-down)" 또는 "단방향식" 데이터 흐름이라고 한다. 모든 state는 항상 특정한 컴포넌트가 소유하고 있으며 그 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 "아래"에 있는 컴포넌트에만 영향을 미친다.

 

트리구조가 props들의 폭포라고 상상하면 각 컴포넌트의 state는 임의의 점에서 만나지만 동시에 아래로 흐르는 부가적인 수원(water source)라고 할 수 있다.

 

➡ 데이터가 아래로 흐른다!

 

state 끌어올리기 

여기에 A, B, C component가 있다.

코드 상으로는 아래와 같다.

function A(){
	return (
    	<>
    	    <B />
            <C />
    	</>
    )
}

데이터는 위에서 아래서 흐른다고 설명했었다.

하지만 C 컴포넌트가 B 컴포넌트의 state가 필요할 때는 어떻게 해야할까?

B 컴포넌트의 state를 A 컴포넌트의 위치에 올림으로써 

B와 C 컴포넌트는 A 컴포넌트에게 props로 state값을 받을 수 있다.

➡ state 끌어올리기!

 

🤔 state 끌어올리기는 무엇이 좋을까?

단일 진실 공급원(Single source of truth) : 모든 데이터 요소를 한 곳에서만 제어 또는 편집하도록 조직하는 관례

1. 단일 진실 공급원 설계 방법을 사용할 수 있다.

2. 데이터 단방향 흐름을 지향하는 react와도 일맥상통 될 수 있다.

 

만약, 정보를 받는 것 이외에 정보를 변경하고 싶다면 어떻게 해야할까?

props로 state뿐만 아니라 setState() 함수도 내려받으면 된다!

 

아래의 요구사항을 구현한다고 생각해보자.

C와 F input 간에 서로 state를 간섭하지 않기 때문에 데이터 단방향 흐름대로 설계할 수 있다.

 

하지만 요구사항이 아래와 같이 변경되었다고 가정해보자.

C와 F 값 간에 서로 영향을 미치기 때문에 state 끌어올리기를 해야 한다.

➡ 각각 state를 갖고 있는 것이 아닌, 상위 컴포넌트에서 state를 관리해야 한다.