Dog foot print

useState 조금 더 자세히 사용해보기 본문

REACT

useState 조금 더 자세히 사용해보기

개 발자국 2023. 3. 23. 09:04

컴포넌트내에서의 상태 갱신

React를 접하는 이들이 가장 먼저 사용해보는 컴포넌트의 상태를 관리하는 Hook이다. 아래의 간단한 코드를 보자. 아래의 예제는 두개의 버튼을 두어, numb라는 값을 1씩 증가시키거나, 감소시킨다.

import React from 'react'

export default function App() {

    let numb = 0;

    return (
        <div>
            <div className='numb'>
                {numb}
            </div>
            <div className='buttons'>
            <button onClick={() => {
                numb += 1
                console.log(numb)
            }}>
                Increase
            </button>
            <button onClick={() => {
                numb -= 1
                console.log(numb)
            }}>
                Decrease
            </button>
            </div>
        </div>
    )
}

아래는 결과이다. 우리는 화면에 numb가 버튼의 클릭에 따라 갱신되기를 기대했지만, 화면에 갱신은 되지 않지만, 값은 실제로 변하고 있다.

 

함수형 컴포넌트에서 돔(화면)을 업데이트하는 것은 [“newProps”, “useState”,”useReducer”,”useContext”] 를 이용하는 것이다. 즉 컴포넌트내부에서 아무리 변수를 변경하여도, 위의 4가지 방법을 이용하지 않고는 컴포넌트가 다시 렌더링 되지 않는다.

 

함수형 컴포넌트도 결국에는 함수이다. 한번 돔을 리턴 한 함수는 다시 한번 호출하지 않으면, 돔을 새롭게 변경 할 수 없다. 그렇기에 다시 한번 함수형 컴포넌트를 호출 할 수 있는 방법을 사용해서, 새롭게 렌더링해야한다.

useState 사용해보기

import React,{useState} from 'react'

export default function App() {

    const [numb, setNumb] = useState(0);
        return (...)
}

useState는 react 라이브러리에서 가져와 사용 할 수 있다. **useState는 파라메터로 기본 값을 전달하고, 현재 상태의 값과 이 상태를 업데이트 할 수 있는 set 함수를 담은 배열을 반환한다. 보통 이 반환된 값과 함수의 명명 규칙은 [상태, set상태] 의 형태를 취한다.

리턴되는 값이 배열(튜플)이기에 아래와 같이 사용 할 수 있지만, 배열 구조 할당을 이용해서, 훨씬 가독성이 좋은 코드를 쓸 수 있기 때문에, 사용하지는 않는다.

import React,{useState} from 'react'

export default function App() {

    const numbState = useState(0);


    return (
        <div>
            <div className='numb'>
                {numbState[0]}
            </div>
            ...
            )
}

반환된 상태값과 업데이트 함수를 사용하여, 아래와 같이 예제를 완성 할 수 있다.

import React,{useState} from 'react'

export default function App() {

    const [numb, setNumb] = useState(0);


    return (
        <div>
            <div className='numb'>
                {numb}
            </div>
            <div className='buttons'>
            <button onClick={() => {
                setNumb(numb + 1)
            }}>
                Increase
            </button>
            <button onClick={() => {
                setNumb(numb - 1)
            }}>
                Decrease
            </button>
            </div>
        </div>
    )
}

 

useStatus로 배열다루기

useState로 배열을 다루는 것도 동일하다. 버튼을 클릭하여, 인풋에 전달된 값을 리스트로 표현해보자.

import React,{useState} from 'react'

export default function App() {

    const [name, setName] = useState("");
    const [users,setUsers] = useState<string[]>(["Lee", "Kim"]);

    function userInsert(name : string){
        setUsers([...users,name])
    }

    return (
        <div>
            <div>
                <input type='text' onChange={(e) => setName(e.target.value)}/>
                <button onClick={() => userInsert(name)}>
                    Insert
                </button>
            </div>
            {users.map((v) => {
                return <div>{v}</div>
            })}
        </div>
    )
}

setState함수를 조금 더 함수형 프로그래밍에 맞게 작성해보자.

현재 배열을 다루기 위해, userInsert 함수는 파라매터로 새로운 name을 받지만 setUsers에서 이전 배열을 다루기위해서, 함수 외부에서 users를 불러와 이전 내용을 새로운 배열에 입력한다. 이처럼 함수내부에서 외부의 지역변수를 참조하는 것을 클로저라고 한다.

클로저를 이용해 이전 값을 활용하는 것은 문제가 되지 않으나, 내부함수가 어딘가에서 계속 유지되고 있다면 이전 값은 스코프가 유지되어 메모리 해제가 발생하지 않을 수 있다. (물론 코드를 깔끔하게 유지하고 이를 신경쓴다면 문제 되지 않을 수 있다.)

아래의 코드는 useState와 setState함수의 타입이다.

 

    function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
    // Unlike the class component setState, the updates are not allowed to be partial
    type SetStateAction<S> = S | ((prevState: S) => S);
    // this technically does accept a second argument, but it's already under a deprecation warning
    // and it's not even released so probably better to not define it.
    type Dispatch<A> = (value: A) => void;
    // Since action _can_ be undefined, dispatch may be called without any parameters.

 

위의 dispatch함수를 보면, 2가지의 활용 방법이 존재한다. 하나는 값을 바로 집어넣어 상태를 업데이트 하는 것이고 다른 한가지는 콜백함수를 이용해서, 이전 값을 파라메터로 받는 콜백함수를 전달 하는 것이다.

콜백 함수를 이용해 이전 값에 접근하도록 함수를 변경해보자.

 

import React,{useState} from 'react'

export default function App() {

    const [name, setName] = useState("");
    const [users,setUsers] = useState<string[]>(["Lee", "Kim"]);

    function userInsert(name : string){
        setUsers((prevState) => [...prevState,name])
    }

    return (
        <div>
            <div>
                <input type='text' onChange={(e) => setName(e.target.value)}/>
                <button onClick={() => userInsert(name)}>
                    Insert
                </button>
            </div>
            {users.map((v) => {
                return <div>{v}</div>
            })}
        </div>
    )
}

이처럼 상태를 변경하기 위해 이전 값에 접근 하는 경우가 존재한다면, 콜백함수를 이용하도록 하자.

초기 상태값 다루기

함수형 컴포넌트는 파라메터를 이용하여, 리액트 엘리먼트를 반환하는 순수 함수이기 때문에, 상태변화나 프롭스가 변경되어 리렌더링 된다는 의미는 함수를 다시 호출 한다는 의미이다.

배열의 길이를 계산하는 함수를 O(n)으로 설정하고 이를 최초 초기값으로 넣어 보자.

import React,{useState} from 'react'

function hardComputingFunction(arr : any[]){
    // O(n)의 시간이 걸리는 작업.

    console.log("Hard Computeing start")
    let count = 0;
    for(const _ of arr) count +=1;
    return count
}

const defaultUsers = ["Lee", "Kim"];

export default function App() {

    const [name, setName] = useState("");
    const [users,setUsers] = useState<string[]>(defaultUsers);
    const [userLength, setUserLength] = useState(hardComputingFunction(defaultUsers));

    function userInsert(name : string){
        setUsers((prevState) => [...prevState,name])
        setUserLength((prevState) => prevState + 1);
    }

    return (
        <div>
            <div>
                <input type='text' onChange={(e) => setName(e.target.value)}/>
                <button onClick={() => userInsert(name)}>
                    Insert
                </button>
                Now : {userLength}
            </div>

            {users.map((v) => {
                return <div>{v}</div>
            })}
        </div>
    )
}

 

상태가 변경될 때 마다 계속해서, 초기값으로 전달했던 함수가 실행되는 것을 볼 수 있다. 이는 위에서 언급 하였듯이, 렌더링되는 과정에서 함수 컴포넌트를 호출 하고 함수내에 존재하는 모든 구문이 다시 시작되기 때문에 발생하는 것이다.

 

위와 같은 문제를 해결하는 방법은 lazy initializtion을 사용하는 것이다. 뭔가 이름은 거창하지만 실제로는 적용하는데 어려움이 없다. lazy initializtion은 useState 초기값에 복잡한 연산을 포함할 때, 사용되는 것이며 오직 state를 처음 만들어 질때만 실행된다. 이를 사용한다면 다시 렌더링이 되어 초기값을 계산할 때, 함수를 호출하지 않는다.

 

이를 구현하는 방법은 함수의 값을 전달하는 것이아니라, 콜백을 전달하여 리턴 값으로 해당 함수를 반환하도록 하는 방법이다.

 

아래는 “게으른 초기화”를 적용한 코드와 결과이다.

import React,{useState} from 'react'

function hardComputingFunction(arr : any[]){
    // O(n)의 시간이 걸리는 작업.

    console.log("Hard Computeing start")
    let count = 0;
    for(const _ of arr) count +=1;
    return count
}

const defaultUsers = ["Lee", "Kim"];

export default function App() {

    const [name, setName] = useState("");
    const [users,setUsers] = useState<string[]>(defaultUsers);
    const [userLength, setUserLength] = useState(() => hardComputingFunction(defaultUsers));

    function userInsert(name : string){
        setUsers((prevState) => [...prevState,name])
        setUserLength((prevState) => prevState + 1);
    }

    return (
        <div>
            <div>
                <input type='text' onChange={(e) => setName(e.target.value)}/>
                <button onClick={() => userInsert(name)}>
                    Insert
                </button>
                Now : {userLength}
            </div>
            {users.map((v,i) => {
                return <div key={i}>{v}</div>
            })}
        </div>
    )
}

lazy initialization을 적용 했을 때, 상태가 변경되어 컴포넌트가 계속 렌더링되어도 hardComputingFunction 는 실행되지 않는다.

 

잘못된 오해

이 포스트를 올리기위해, useState의 다양한 블로그 글들을 확인 하였는데 이상한 점이 많이 보였다. 바로 useState 비동기라는 제목으로 돌아다니는 포스트들인데, 대부분의 예시가 다음과 같다.아래는 userInsert 함수를 호출 한 뒤, “setUserLength” 함수에 users.length + 1 을 해서, 최신상태의 users의 길이에 1씩 연속적으로 더해 users.length + 3이 출력되게 끔 하는 목적의 코드이다.

export default function App() {

    const [name, setName] = useState("");
    const [users,setUsers] = useState<string[]>(defaultUsers);
    const [userLength, setUserLength] = useState(() => hardComputingFunction(defaultUsers));

    function userInsert(name : string){
        setUsers((prevState) => [...prevState,name])
        setUserLength(users.length + 1);
        setUserLength(users.length + 1);
        setUserLength(users.length + 1);
    }

    return (
        <div>
            <div>
                <input type='text' onChange={(e) => setName(e.target.value)}/>
                <button onClick={() => userInsert(name)}>
                    Insert
                </button>
                Now : {userLength}
            </div>
            {users.map((v,i) => {
                return <div key={i}>{v}</div>
            })}
        </div>
    )
}

 

이 결과는 변경되기전 users의 업데이트 전 길이와 1을 더한 값이 setUsersLength로 업데이트 된다. 그 이유는 위에서도 언급 하였듯이 함수형 컴포넌트는 함수 일 뿐이며, 값이 변경되어 렌더링 된다는 의미는 다시 한번 이 함수형 컴포넌트를 호출 한다는 의미이다. 그렇기에 setUsers로 변경한 값은 현재 userInsert 함수 스코프내의 users 에 반영되지 않는 것이다.

 

만약 위 목적처럼 새로운 users의 길이에 3을 연속적으로 setUserLength로 구현하기 위해서는 다음과 같이 코드를 작성하면 된다.


export default function App() {

    const [name, setName] = useState("");
    const [users,setUsers] = useState<string[]>(defaultUsers);
    const [userLength, setUserLength] = useState(() => hardComputingFunction(defaultUsers));

    function userInsert(name : string){
        setUsers((prevState) => [...prevState,name])
        setUserLength((prevState) => prevState + 1);
        setUserLength((prevState) => prevState + 1);
        setUserLength((prevState) => prevState + 1);
    }

    return (
       ...
    )
}

 

 

 

setState 함수 내부는 비 동기적으로 작동한다. 이유는 setState 함수를 실행한뒤에 다시 렌더링 하는 함수를 호출 하기 위해 최초 상태 변화가 감지된 다음 16ms 동안 상태 변화를 모두 모아 렌더링 하기 때문이다. (이를 배칭이라 한다.) 그렇기 때문에, 위의 “userInsert” 함수가 모두 끝났을 때 한번만 렌더링 되는 것이다.

반응형
Comments