본문 바로가기

frontend/react

[React] 여러 개의 input 태그 관리와 setState

벨로퍼트 리액트 튜토리얼 input 태그 부분(https://react.vlpt.us/basic/09-multiple-inputs.html)을 공부하는 중에 이해가 안 되는 부분이 있었다.

특히

const onChange = (e) => {
    const { value, name } = e.target;
    setInputs({
        ...inputs, // 기존의 input 객체를 복사
        [name]: value // name 키를 가진 값을 value로 설정
    });
};

이 부분을 이해하기가 어려웠다.

 

☝ 첫 번째로는 ...inputs 

✌ 두 번째로는 [name]: value 부분이다.

강의 자료에 달린 사람들의 댓글과 구글링을 통해 내 나름대로 해당 내용들을 정리할 수 있었다. 


☝ Spread(...) 연산자

우선 ...inputs 에 사용된 ... (spread) 문법에 대해서 먼저 정리해보자.

spread 문법은 객체 혹은 배열을 펼칠 때 사용한다. ...(객체 이름) 형태로 사용하며, 명시된 객체 혹은 배열의 원소를 펼치는 역할을 한다. 여기서 핵심은, spread 문법을 통해 기존의 객체 혹은 배열을 건들이지 않고 새로운 객체 혹은 배열을 생성할 수 있다는 점이다. 

 

const slime = {
    name: '슬라임'
};

const cuteSlime = {
    ...slime,
    attribute: 'cute'
};
    
const purpleCuteSlime = {
    ...cuteSlime,
    color: 'purple'
};

console.log(slime);            // Object {name: "슬라임"}
console.log(cuteSlime);        // Object {name: "슬라임", attribute: "cute"}
console.log(purpleCuteSlime);  // Object {name: "슬라임", attribute: "cute", color: "purple"}

배열에서도 마찬가지로 사용할 수 있다.

const numbers = [1, 2, 3, 4, 5];

const spreadNumbers = [...numbers, 1000, ...numbers];
console.log(spreadNumbers); // [1, 2, 3, 4, 5, 1000, 1, 2, 3, 4, 5]

그래서 왜 setState() 에서 spread 문법을 사용했나? 🤔

 

리액트에서는 setState() 메소드를 이용하여 state 값을 변경하고, 그에 따라 리렌더링 여부를 결정한다.

state를 다룰 때 가장 주의해야 할 점은, 기존의 것을 유지해야한다 ❗는 것(="불변성을 지킨다")이다.

리액트는 state의 변화를 통해 화면을 리렌더링할지 말지를 결정하는데, 특히 이 불변성을 지켜야만 리액트 컴포넌트에서 state 변경을 감지할 수 있다는 것이다.

즉, 이 state 값을 '직접' 변경하게 되면(여기서 '직접 변경한다'는 것은, 객체나 배열 따위를 복사하지 않은 채 원본의 데이터를 수정하는 것을 의미한다. 객체와 배열은 레퍼런스로 참조하기 때문에 특히 이 부분을 더 주의해야한다), 리액트는 state 변화를 인지하지 못한다. 

 

왜 state 값을 '직접' 변경하면 리액트가 변경을 감지할 수 없는지에 대한 이유는 shouldCompomentUpdate() 메소드와 관련되어 있다. 

 

shouldComponentUpdate() ?

setState 메소드가 호출되는 순간, 바로 shouldComponentUpdate() 메소드를 호출하게 되는데 이 메소드의 리턴값이 true인지 false인지에 따라 리렌더링 여부를 결정하는 것이다.

리액트는 개발자가 호출한 setState로 해당 state 값을 변경하기 전에 shouldComponentUpdate 메소드를 호출하여 새로운 state값과 기존의 state 값을 비교하여 렌더링 여부를 결정하도록 기회를 준다. 

하지만, 만약 사용자가 setState를 호출하면서 state값을 "직접" 변경하게 된다면(원본 객체(혹은 배열)의 값을 수정), 원본 객체와 변경된 객체의 값이 이미 서로 동일하기 때문에 리액트에서는 변화를 감지하지 못하는 것이다. 
그렇기 때문에, setState를 통해 state 변경을 할 때는 기존의 객체를 복사하여 state를 재설정해야한다.

 

그리고 추가적으로, 우리는 이 shouldComponentUpdate 메소드를 재정의하여 유동적으로 화면을 렌더링할 수도 있다(=컴포넌트 최적화).

+ 불변성을 지켜야만 나중에 컴포넌트 업데이트 성능을 제대로 최적화할 수 있다. 

 

 

function InputSample() {
    const [inputs, setInputs] = useState({
        name: '',
        nickname: '',
    });

    const { name, nickname } = inputs;

    const onChange = (e) => {
        const { value, name } = e.target;
        setInputs({
            ...inputs, // 기존의 input 객체를 복사
            [name]: value // name 키를 가진 값을 value로 설정
        });
    };
    // ... more codes ...
}

다시 해당 코드에서 setInputs() 부분에 주목하여 살펴보자면,

 

☝ 가장 먼저 spread 연산을 통해 기존 inputs 객체에 저장되어있는 name 과 nickname 값을 복제한다.

✌ 그 중, [name]의 value 값을 사용자가 입력한 값으로 변경한다. 즉, 기존의 값은 유지하되 사용자가 변경한 값만 수정한다. 

 

이제 두 번째 과정에 대해서 정리할 차례다.

 

✌ [key] : we can DYNAMICALLY set the key.

우선 다음과 같은 예제 코드가 있다고 가정해보자.

import React from 'react'

class BasicForm extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      firstName: '',
      age: '',
    }
  }

  handleFirstNameChange = (event) => {
    const firstName = event.target.value
    this.setState({ firstName: firstName })
  }

  handleAgeChange = (event) => {
    const age = event.target.value
    this.setState({ age: age })
  }

  handleSubmit = (event) => {
    event.preventDefault()

    const {
      firstName,
      age,
    } = this.state

    console.log(`A first name was submitted: ${firstName}. An age was submitted: ${age}`)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          First Name:
          <input
            name="firstName"
            type="text"
            value={this.state.firstName}
            onChange={this.handleFirstNameChange} />
        </label>
        <label>
          Age:
          <input
            name="age"
            type="number"
            value={this.state.age}
            onChange={this.handleAgeChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

export { BasicForm }

 

🤨 뭐 여기까지는 괜찮아보인다... 하지만, form의 수가 많아진다면, 선언해야하는 메소드의 양도 그만큼 증가할 것이다. 예를들어 handleLocationChange라던지 handleHeightChange라던지 handleLastNameChange 등등..🤯 결국 비슷한 코드의 반복이 기하급수적으로 늘어날 것이다.

 

이 문제를 해결해주는 것이 바로 [name]: value 이다. 우리는 이 문법을 통해 제너릭한 handleChange 메소드를 생성할 수 있다. 정리하자면,이 문법을 통해 input 태그의 name 속성에 기반하여, 동적으로 각 name이 가리키는 state 값을 "하나의 메소드에서" 충분히 조작할 수 있다. 

 

말이 좀 어려우니 예제를 살펴보자

import React from 'react'

class BasicFormRefactored extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      firstName: '',
      age: '',
    }
  }

  handleChange = (event) => {
    const { name, value } = event.target;
    this.setState({ 
      [name]: value,
    });
  }

  handleSubmit = (event) => {
    event.preventDefault()

    const {
      firstName,
      age,
    } = this.state

    console.log(`A first name was submitted: ${firstName}. An age was submitted: ${age}`)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          First Name:
          <input
            name="firstName"
            type="text"
            value={this.state.firstName}
            onChange={this.handleChange} />
        </label>
        <label>
          Age:
          <input
            name="age"
            type="number"
            value={this.state.age}
            onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

export { BasicFormRefactored }

 

보다시피 분산되어있던 여러 개의 handleChange 메소드들이 다음과 같이 단 하나의 메소드로 통합된 것을 살펴볼 수 있다.

handleChange = (event) => {
    const { name, value } = event.target;
    this.setState({ 
        [name]: value,
    });
}

☝ 이벤트를 발생시킨 태그의 name 값을 참조하여,

해당 name 의 state 값을 사용자가 입력한 value로 바꾸는 것이다. 

 

즉, 이 문법을 통해 여러 개로 분산되어있던 handleChange 메소드를 한 곳으로 통합시킬 수 있는 것이다.

앞서 살펴봤던 코드를 통해서도 이를 다시 상기할 수 있다.

 

// 사용자의 입력을 받아오는 input 태그의 상태 관리
import React, { useState } from 'react';

function InputSample() {
    const [inputs, setInputs] = useState({
        name: '',
        nickname: '',
    });

    const { name, nickname } = inputs;

    const onChange = (e) => {
        const { value, name } = e.target;
        setInputs({
            ...inputs, // 기존의 input 객체를 복사
            [name]: value // name 키를 가진 값을 value로 설정
        });
    };

    const onReset = () => {
        setInputs({
            name: '',
            nickname: '',
        });
    };

    return (
        <div>
            <input placeholder="이름" onChange={onChange} value={name}/>
            <input placeholder="닉네임" onChange={onChange} value={nickname}/>
            <button onClick={onReset}>초기화</button>
            <div>
                <b>값: </b>
                {name} ({nickname})
            </div>
        </div>
    );
}

export default InputSample;

보다시피 e.target을 통해 이벤트가 발생한 input 태그의 정보를 불러오고, setInputs({ [name]: value }) 문법을 통해,

이름과 닉네임을 사용자가 input 태그에 각각 입력한 값으로 변경해주는 작업(=onNameChange, onNicknameChange)을 따로 분리하지 않고 onChange 메소드 한 곳에서만 처리하는 것을 볼 수 있다. 

참고로 여기서 각괄호[]를 쓰지 않고 name: value로 작성하게 되면, 키 자체를 'name' 으로 인식한다. 즉, 닉네임 input란에 닉네임을 새로 입력해도 닉네임 값은 변경되지 않고 이름 값만 변경되니 주의하자.

 

 

 

이에 대한 추가적인 내용은 https://medium.com/@bretdoucette/understanding-this-setstate-name-value-a5ef7b4ea2b4 를 참고하면 좋을 듯하다.

 

 


참고:
https://learnjs.vlpt.us/useful/07-spread-and-rest.html

https://react.vlpt.us/basic/09-multiple-inputs.html

 

https://seungddak.tistory.com/109

https://xiubindev.tistory.com/97

https://medium.com/@bretdoucette/understanding-this-setstate-name-value-a5ef7b4ea2b4

 

'frontend > react' 카테고리의 다른 글

[React] React Hook 사용하기 - useMemo, useCallback  (1) 2022.12.07