[React] 2. State

100tick·2022년 12월 13일
0
post-thumbnail

State란 무엇인가

웹페이지에는 불변/가변 데이터가 존재한다.

그 중에서도 변할 수 있는 가변 데이터를 React에서는 State라고 부르며,

useState()라는 특별한 함수를 통해 생성할 수 있다.

이해를 돕기 위해 https://wikipedia.org에 접속해보자.

위키피디아의 index페이지다.

해당 페이지에 전세계 그 누가 접속해도 같은 화면을 보여줄 것이다.

이렇게 언제나 똑같은 데이터를 보여주며 변할 필요가 없는 데이터들은 State가 될 필요가 없다.

바뀔 일이 없으니 Plain Text 형태로 전달해도 충분할 것이다.

그러나 아래와 같은 경우, State가 필요하다.

검색창에 서울이라고 입력하는 순간, 검색 결과가 나타나는 것을 볼 수 있다.

만약 Wikipedia가 React로 만들어진 웹페이지이며

검색어를 저장할 변수를 search라고 가정하면,

처음에는 검색창에 아무런 글자도 입력되지 않은 상태였기 때문에

빈 문자열(search == "";)이었을 것이다.

그러나 서울을 검색하는 순간, search == "서울";이 되며,

search 의 상태가 변화 되었으며, 빈 문자열이 아님을 감지하여 검색 결과를 보여주는 방식으로 비슷한 기능을 구현할 수 있었을 것이다.

useState()를 호출하여 State 생성

const [data ,setData] = useState();

// same as above
const v = useState(); // returns [data, mutate_fn]
const data = v[0];
const setData = v[1];

// Array Destructuring assignment 문법을 사용했기 때문에
// 배열 내부의 요소들이 바로 변수에 bind 됨

Destructuring assignment: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

React에서 State를 만들 때는 useState() 함수를 실행하여 반환 받은 값을 사용해야 한다.

useState() 는 배열을 반환한다.

index 0에는 State가 들어있고,

index 1에는 State를 변경할 수 있는 mutate fn이 들어있다.

주의할 점은, State를 변경할 때, 직접 변경하면 안 된다는 것이다.

예를 들어 data(State)가 number type이며, 100을 대입하고 싶다고 하면,

data = 100; 과 같이 일반적인 변수를 변경하듯 하면 안 된다는 것이다.

CodeSandBox Example1

https://codesandbox.io/s/ecstatic-jennings-uk11nx?file=/src/App.js

위 링크에 작성된 예시를 통해 변수를 직접 변경해도 화면이 변하지 않는 것을 확인할 수 있다.

setState를 통한 State 변경

const [data, setData] = useState(""); // 기본값 == ""

setData("New Data"); // State를 "New Data"로 변경

useState() 로부터 반환 받은 배열의 index 1에 있는 요소, setState() 함수에 새로운 State를 인자로 주고 호출한다.

React는 setState() 함수에 새로운 data를 넣어 호출하면 Component를 re-render 한다.

당연하지만 일반적인 JS 변수를 변경해도 React에서 re-render이 일어나지 않는다.
(아래 CodeSandBox Example 참조)

CodeSandBox Example2

https://codesandbox.io/s/still-wave-7xw6r7?file=/src/App.js

배열, 객체 등의 Reference Type을 State로 사용할 때

State를 사용할 때 주의할 점이 있다.

State는 항상 새로운 값이 들어가야 한다는 것이다.

좀 더 정확히 말하면, 다른 메모리 공간을 갖는 값이 들어가야 한다.

let a = 1;
let b = a;

b = 10;
console.log(a, b); // 1 10

number, boolean 등의 Primitive Type 변수들은 해당 변수를 다른 변수에 대입하더라도 새로운 값으로 Copy된다.

즉, 아예 다른 메모리 주소를 갖게 된다.

고로 b에 a를 대입하고, b를 변경하더라도 a는 변경되지 않는다.

let obj1 = { value: 1 };
let obj2 = obj1;

obj2.value = 10;
console.log(obj1, obj2);

그러나 Array, Object 등의 Reference Type 변수를 생성하고 다른 변수에 대입하는 경우, 같은 메모리 공간을 공유하는 Swallow Copy가 일어난다.

얕은 복사 라고도 부른다.

이렇게 구현된 이유는 배열로 예를 들자면 배열 내부에 얼마나 많은 요소가 들어갈지 예상할 수 없기 때문이다.

function printArray(array) {
    for (elem in array) {
        console.log(elem);
    }
}

const someArray = []; // 수 억 개의 elements가 있다고 가정
printArray(someArray)

만약 someArray의 요소가 수 억개라면, printArray()의 인자로 전달될 때 모든 요소에 대한 복사가 일어날텐데, 이 때 모든 수 억 개의 요소를 재생성 할 것이다.

그러나 console.log()로 요소를 출력하기만 하는 상황에서 똑같은 배열을 하나 더 만들 필요는 없다.

이는 매우 비효율적이므로, 대부분의 언어에서 배열, 객체 등의 size가 유동적인 타입의 값들은 기본적으로 주소만 복사하고 데이터는 공유하도록 Swallow Copy로 구현된다.

React는 State와 다음 State의 주솟값을 비교한다

setState()가 실행되면 React는 re-render을 할지 말지 결정하기 위해서 이전 State와 새로운 State의 주솟값을 비교한다.

let userData = { name: "a" };
const [user, setUser] = useState(user);
let userData2 = userData;
userData2.name = "b";
setUser(userData);

위에서 봤듯이, 만약 배열 등의 Reference Type의 값들을 새로운 변수에 대입하여도,
같은 객체를 참조하기 때문에 주소값 역시 같다.

위의 코드에서 userData2userData 를 대입했지만 같은 객체를 가리키는 Reference이므로, React는 해당 객체에 변화가 없다고 인식한다.

심지어 userData2.name = "b"; 로 원본 객체를 수정하기까지 했는데도 ReactReference Types에 대해 re-render를 판단하는 기준은 "주소값이 변했는가"의 여부이기 때문에 화면 상으로 어떠한 변화도 일어나지 않는다.

단, background에서 userData, userData2가 가리키는 객체의 name 값은 "b"로 변경된 상태이다. React가 변화를 인식하지 못해 화면에 반영이 안 될 뿐이다.

https://codesandbox.io/s/upbeat-joliot-dlzr95?file=/src/App.js

import React from 'react';

import React from 'react';

const Component1 = () => {
    return <div></div>;
    // -> return React.createElement('div', ...);
}

예전의 React에서는 각 js, jsx 파일마다 명시적으로 import 해야 했다.
<div><div>와 같은 JSX 구문은 React.createElement();로 변환된다.

const Component1 = () => {
    return <div></div>;
    // import {jsx} from "react"; (auto import)
    // -> return jsx('div', ...);
}

현재는 위와 같이 바뀌었기 때문에 React를 명시적으로 import 할 필요가 없다고 한다.

그러나 언젠가 예전 코드를 다루게 될 경우가 생길 수 있기 때문에 React를 import 하지 않아서 오류가 생길 수 있다는 사실도 알아두도록 하자.

State을 Props로 전달하여 Child Component로 업데이트

https://codesandbox.io/s/autumn-butterfly-m1nixl?file=/src/App.js

import { useState } from 'react';

const Parent = () => {
    const [count, setCount] = useState(0);
    
    const add = () => {
        setCount(prev => prev + 1);
        // prev는 이전 상태의 데이터
        // setCount(count + 1); 과 같음
    }

    const subtract = () => {
        setCount(prev => prev - 1);
        // setCount(count - 1); 과 같음
    }

    const init = () => {
        setCount(0);
    }

    return <Child count={count}/>
}

// object destructuring
// let props = {count, add, subtract, init};
// let { count, add, subtract, init} = props;
const Child = ({count, add, subtract, init}) => {
    return (
        <div>
            <h1>{count}</h1>
            <button onClick={add}>+</button>
            <button onClick={subtract}>-</button>
            <button onClick={init}>초기화</button>
        </div>
    );

}

Stateful Component, Stateless Component

위에서 봤던 코드를 쪼개서 다시 보도록 하자.

// Stateful Component
import { useState } from 'react';

const Parent = () => {
    const [count, setCount] = useState(0);
    
    const add = () => {
        setCount(prev => prev + 1);
        // prev는 이전 상태의 데이터
        // setCount(count + 1); 과 같음
    }

    const subtract = () => {
        setCount(prev => prev - 1);
        // setCount(count - 1); 과 같음
    }

    const init = () => {
        setCount(0);
    }

    return <Child count={count}/>
}

Parent 컴포넌트는 useState() 함수를 통해 State를 생성하였고, 이를 변경할 수 있는 mutate fn도 가지고 있다.

이렇게 직접 State를 다루고 갱신할 수 있는 컴포넌트를 Stateful Component라고 한다.

// Stateless Component
const Child = ({count, add, subtract, init}) => {
    return (
        <div>
            <h1>{count}</h1>
            <button onClick={add}>+</button>
            <button onClick={subtract}>-</button>
            <button onClick={init}>초기화</button>
        </div>
    );

}

Child 컴포넌트에는 useState() 함수가 없으며, Parent 컴포넌트로부터 받아온 props만 존재합니다.
State가 없는 Stateless Component의 모습이다.

그러나 Parent로부터 받아온 add(), subtract(), init() 등을 호출하면 Parent에게 State를 업데이트 하라는 요청을 할 수 있다.

그로 인해 Parent에서 업데이트가 일어나게 되고,
React는 변화를 감지하고 Parentcount State를 업데이트 한다.

기존 Parent는 폐기되고 re-render 되며, count라는 props를 의존하고 있던 Child 역시 폐기되고 re-render 된다.

처음에 존재하던 ParentParent1, ChildChild1이라고 하면, Child에서 add() 함수를 호출하는 순간 Parent1 -> Parent2로 완전히 새로운 컴포넌트가 재생성 된다.

이 과정에서 Child1 -> Child2로 역시 재생성 된다.

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

즉, React에서 State를 업데이트 하는 것은 해당 State를 가진 컴포넌트를 새로 생성하도록 만든다.

또한 해당 컴포넌트의 Stateprops로 전달 받은 하위 컴포넌트까지도 새로 생성된다.

컴포넌트, 그리고 State 값을 변경하지 않고 완전히 새로운 값을 주는 것이다.

이러한 이유로 State를 생성할 때 const 키워드를 사용한다.

한 번 생성되면 그 값을 변경할 수 없다는 뜻인데,

아래 예제를 확인하면 빠르게 이해할 수 있다.

https://codesandbox.io/s/distracted-estrela-jfy38c?file=/src/App.js

count++ 에 의해 const로 선언된 State가 직접 변경되었고 그로 인해 오류가 발생했다.

오류는 개발자가 계속해서 마주해야 하는 벽이지만, 동시에 어두운 길을 밝혀주는 등불이 되어주기도 한다.

React에서 State를 직접적으로 변경하는 것은 하면 안 되는 일이다.

그래서 실수로 State를 직접 변경하더라도 const로 선언했기 때문에 오류를 재빠르게 파악하고 바로 잡을 수 있다.

추가로 Stateful ComponentContainer Component 또는 Smart Component라고도 불리며 Stateless ComponentDumb Component 또는 Presentational Component라고도 불린다.

React에 영향을 받아 탄생한 수많은 Frontend Library, Framework들이 있는데,

기본 개념, 동작 원리가 React와 상당히 유사한 부분이 많고

심지어 명칭, 용어도 비슷한 부분이 많기 때문에 React를 학습한다면 나중에 다른 것을 사용해야 될 때가 온다고 해도 상당히 빠르게 도움이 많이 될 것 같다.

// Flutter에서 사용하는 StatelessWidget
//
// dart언어로 작성되며 React에서는 Component,
//
// Flutter에서는 Widget이라고 부름
class Frog extends StatelessWidget {
  const Frog({
    super.key,
    this.color = const Color(0xFF2DBD3A),
    this.child,
  });

  final Color color;
  final Widget? child;

  
  Widget build(BuildContext context) {
    // Props를 Container라는 Widget(React에서 Component)로 전달
    return Container(color: color, child: child);
  }
}
class YellowBird extends StatefulWidget {
  const YellowBird({ super.key });

  
  State<YellowBird> createState() => _YellowBirdState();
}

class _YellowBirdState extends State<YellowBird> {
  
  Widget build(BuildContext context) {
    return Container(color: const Color(0xFFFFE306));
  }
}
// Rust에서 가장 유명한 Frontend 라이브러리 Yew.rs
use std::fmt::Display;
// react와 마찬가지로 function_component라는 이름으로 불리는 것을 볼 수 있음
use yew::{function_component, html, Properties};

// React와 유사한 Props의 형태
#[derive(Properties, PartialEq)]
pub struct Props<T>
where
    T: PartialEq,
{
    data: T,
}

#[function_component(MyGenericComponent)]
pub fn my_generic_component<T>(props: &Props<T>) -> Html
where
    T: PartialEq + Display,
{
    // jsx와 유사한 문법을 매크로 라는 것으로 구현하였음
    html! {
        <p>
            { &props.data }
        </p>
    }
}

// used like this
html! {
    <MyGenericComponent<i32> data=123 />
};

// or
html! {
    <MyGenericComponent<String> data={"foo".to_string()} />
};
use yew::{Callback, function_component, html, use_state};

#[function_component(UseState)]
fn state() -> Html {
    // use_state
    let counter = use_state(|| 0);
    let on_click = {
        let counter = counter.clone();
        Callback::from(move |_| counter.set(*counter + 1))
    };


    html! {
        <div>
            <button {onclick}>{ "Increment value" }</button>
            <p>
                <b>{ "Current value: " }</b>
                { *counter }
            </p>
        </div>
    }
}

0개의 댓글