[React] 2.

zdpk·2023년 2월 22일

React

목록 보기
2/3
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개의 댓글