useState Hook

박종한·2020년 3월 31일
0

React

목록 보기
15/17

컴포넌트에서 보여줘야 하는 내용이 사용자의 입력값에 따라 바뀌어야 할때 사용

Hook기능이 16.8 이후부터 도입되면서 함수형 컴포넌트에서도 state값의 변경에 대한 구현이 가능해짐

카운터 프로그램을 예시로 들어보겠음

import React, { useState } from 'react';

function Counter() {
    const [counter, setCounter] = useState(0);
    const onIncrease = () => {
        setCounter(counter + 1);
    }
    const onDecrease = () => {
        setCounter(counter - 1);
    }
    return (
        <>
            <h1>{counter}</h1>
            <button onClick={onIncrease}>+1</button>
            <button onClick={onDecrease}>-1</button>
        </>
    );
}

export default Counter;

우선 눈여겨볼 점은
buttononClick에 의해 리스너가 걸려있음
즉, 해당 버튼이 클릭될 경우 클릭이벤트가 발생하고, 그 이벤트에 대한 처리를 해주는 것이 각각 onIncreaseonDecrease

따라서, 우리는 이 이벤트핸들러인 onIncreaseonDecrease에서 state를 변경한다면, 그를 통해 REACT는 리렌더링을 진행하게 되고, 이것은 동적인 상태 관리가 가능해진다는 것을 의미

실제 결과값은 위와 같고 +1을 누를 경우, 숫자가 1씩 증가하고, -1을 누를 경우는 1씩 감소함

여기서 주의할 점이 하나 있는데, useState를 사용할 경우, 배열이 반환되는데, 비구조화 할당, 즉 디스럭쳐링을 통해, 바뀌는 상태값과, 상태값을 바꾸는 함수가 반환이 된다.

const [counter, setCounter] = useState(0);

여기서 counter는 바뀌는 상태값이고,
setCountercounter를 바뀌게 만드는(업데이트하는) 함수고,
useState(0)는 동적인 상태관리를 사용하겠다는 것을 의미함과 동시에 초기값으로 0을 준다는 것을 뜻한다.

함수형 업데이트

setCounter 함수에는 위에 처럼, ()안에 그냥 바뀌는 값을 넣어줄 수도 있지만, 다른 방식으로는 함수형 업데이트를 넣어줄 수도 있다.

그래서 함수형 업데이트가 뭔데?

현재값 => 현재값을 조작하는 내용과 같이 작성하면 되는데,

setCounter(prevCounter => prevCounter+1) 이렇게 작성해도 된다는 얘기다.
위 코드는
setCounter(counter+1)과 완전히 동일한 결과를 가져온다.

그리고 참고로 prevNumber대신 n같이 아무거나 집어넣어도 상관이 없다.

그리고, 위 경우는 단순한 예시기 때문에 return이 없었지만,setCounter(prevCounter => {...함수내용 + return something})처럼 함수를 넣어줄 수도 있다.

그렇다면 왜 쓰는데?

https://velog.io/@johnque/useState-%ED%95%A8%EC%88%98%ED%98%95-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8
위 링크 참조

또 다른 예시

이번에는 Counter가 아니라 input을 처리하는 리액트 컴포넌트를 만들어보겠음

내가 입력한 값에 따라 실시간으로 아래 값이 바뀌고, init버튼을 클릭하면 두가지 다 시도를 해볼수가 있는데,
1. value: 오른쪽의 문자열만 초기화가 된다.
2. input에 있는 것까지 같이 초기화가 된다.

위 둘의 차이는 단순히 input에 value를 넣어주느냐 아니냐의 차이인데, 코드는 아래와 같다.

import React, { useState } from 'react';

function InputSample() {
    const [string, setString] = useState('');
    const onChange = event => {
        setString(event.target.value);
    }
    const onInit = () => {
        setString('');
    }
    return (
        <>
            <input type="text" onChange={onChange} value={string} />
            <button onClick={onInit}>init</button>
            <div>
                <b>value: </b>
                {string}
            </div>
        </>
    )
}

export default InputSample;

input에 value={string}을 넣어주지 않았다면 버튼이 눌려 onInit 함수가 호출되도 input값은 초기화되지 않고, value: 옆의 문자열만 초기화가 된다.
그러나, 넣어주게 되면 같이 초기화가 된다.

input리스너의 경우 매개변수로 event를 받게 되는데, event.target.name은 현재 input에 아무런 이름이 없기 때문에 빈칸이 출력되고, event.target.value만이 onChange에 의해 실시간으로 값이 갱신된다.

실제로 console창에 name과 value를 둘다 찍어봐도 name만 빈칸으로 나와 한칸이 띄어져 있음을 알 수 있다.

<input name="input">이런식으로 name을 넣어주면 위와같이 출력되는 것을 볼 수 있다.

또 다른 예시 2

그렇다면 input이 여러개라면 어떡할 것인가?

다음과 같은 경우라면 말이다.

여기서는, setString()의 괄호안에 객체를 넣어줘야 한다. 그렇게 하지 않으면 두 input을 관리해줄 수가 없다.

import React, { useState } from 'react';

function InputSample() {
    const [string, setString] = useState({
        height: '',
        weight: '',
    });
    const onChange = event => {
        setString({
            ...string,
            [event.target.name]: event.target.value
        });
    }
    const onInit = () => {
        setString({
            height: '',
            weight: '',
        });
    }
    return (
        <>
            <div>
                <label htmlFor="height">:</label>
                <input type="input" onChange={onChange} value={string.height} name="height" />
            </div>
            <div>
                <label htmlFor="weight">몸무게:</label>
                <input type="input" onChange={onChange} value={string.weight} name="weight" />
            </div>
            <button onClick={onInit}>init</button>
            <div>
                <b>height: </b>
                {string.height}
            </div>
            <div>
                <b>weight: </b>
                {string.weight}
            </div>
        </>
    )
}

export default InputSample;

위 코드를 찬찬히 살펴보면

    const [string, setString] = useState({
        height: '',
        weight: '',
    });
    const onChange = event => {
        setString({
            ...string,
            [event.target.name]: event.target.value
        });
    }

이렇게 useState()안의 초기값이 객체로, 두 개의 property가 들어가 있고,
이 경우 string = { height: '', weight: '' }이 된다.
onChange리스너를 보면, event를 받아서, []를 사용해 값을 넣어주는 걸 볼 수 있다.
여기서 []의 의미는, 예를 들어 내가 height input에 숫자를 입력하면 height: 내가 입력한 숫자가 된다.

    const onChange = event => {
        setString({
            ...string,
            height: '185'
        });
    }

이런식으로 말이다. weight도 마찬가지. 즉 기존에 ''였던 height'185'로 바꾸란 의미이다.

그러면, ...string은 뭘까? 이것은 스프레드(Spread) 연산자라고 부르는데,
...을 붙이고 뒤에 객체이름을 붙이게 될경우, 객체의 모든 프로퍼티를 저 하나의 문자로 가져오겠다는 소리인데, 한마디로 위 문장은 아래와 똑같다.

    const onChange = event => {
        setString({
            height: '',
            weight: '',
            height: '185'
        });
    }

string객체의 현재 초기화된 기본값이 { height: '', weight: '' }니까, ...string을 하게되면, height: '', weight: ''를 가져오게 된다. 거기에, height: '185'가 마지막에 와서, 결국 '''185'로 덮어쓰게 된다.

현재는 그럼 string = {height: '185', weight: ''}인 상태이고, weight 역시 다음처럼 바꿔주면 된다.

    const onChange = event => {
        setString({
            ...string,
            weight: '75'
        });
    }

그럼 string = {height: '185', weight: '75'}가 된다.
왜 굳이 이런 불편한 방식들을 써야 하냐면, 리액트에서는 불변성을 지키는게 아주아주 중요하다.
왜냐면 불변성은 리렌더링과 연관이 있기 때문이다. 리렌더링 시키려면 useState()에서 첫번째로 할당된 string의 상태가 변경되어야 하는데, 이 상태는 두번째로 할당된 setString()함수를 통해서만 변경 + 변경감지를 할 수 있고, setString()을 통해서가 아닌 string직접 변경시엔 리렌더링이 발생하지 않는다.

import React, { useState } from 'react';

function InputSample() {
    const [string, setString] = useState({
        height: '',
        weight: '',
    });
    const onChange = event => {
        setString({
            ...string,
            [event.target.name]: event.target.value
        });
    }
    const onInit = () => {
        setString({
            height: '',
            weight: '',
        });
    }
    return (
        <>
            <div>
                <label htmlFor="height">:</label>
                <input type="input" onChange={onChange} value={string.height} name="height" />
            </div>
            <div>
                <label htmlFor="weight">몸무게:</label>
                <input type="input" onChange={onChange} value={string.weight} name="weight" />
            </div>
            <button onClick={onInit}>init</button>
            <div>
                <b>height: </b>
                {string.height}
            </div>
            <div>
                <b>weight: </b>
                {string.weight}
            </div>
        </>
    )
}

그런데 위 코드에서 다소 불편한게 몇 가지 있다.
1. {string.height} 처럼 string이 자꾸 앞에 붙는다.
2. onChange의[event.target.name]: event.target.value 이 부분이 다소 긴 것 같다.

1번부터 해결하자면, 그저 const { height, weight } = string;으로 해결할 수 있다.
이렇게 하면 상태가 동적으로 변경될 경우, 변경된 값이 string에 들어가면서 자동으로 height, weight에도 변경된 값이 들어간다. 이는 값이 변경될 경우, 사용자가 명시하지 않는 이상 무조건 리렌더링이 일어나기 때문에 일어나는 시점에서 string의 변경된 값이 들어가는 것 같다.

    const [string, setString] = useState({
        height: '',
        weight: '',
    });
    const { height, weight } = string;

그냥 이렇게 useState() 다음에 return() 전에 아무데나 써주면 된다.

2번의 경우도 마찬가지로, 비구조화할당(Destructuring)으로 해결이 가능한데,

    const onChange = event => {
        const { name, value } = event.target;
        setString({
            ...string,
            [name]: value
        });
    }

이렇게 써주면 된다.

전체코드는 다음과 같다.

import React, { useState } from 'react';

function InputSample() {
    const [string, setString] = useState({
        height: '',
        weight: '',
    });
    const { height, weight } = string;
    const onChange = event => {
        const { name, value } = event.target;
        setString({
            ...string,
            [name]: value
        });
    }
    const onInit = () => {
        setString({
            height: '',
            weight: '',
        });
    }
    return (
        <>
            <div>
                <label htmlFor="height">:</label>
                <input type="input" onChange={onChange} value={height} name="height" />
            </div>
            <div>
                <label htmlFor="weight">몸무게:</label>
                <input type="input" onChange={onChange} value={weight} name="weight" />
            </div>
            <button onClick={onInit}>init</button>
            <div>
                <b>height: </b>
                {height}
            </div>
            <div>
                <b>weight: </b>
                {weight}
            </div>
        </>
    )
}

export default InputSample;
profile
코딩의 고수가 되고 싶은 종한이

0개의 댓글