본문 바로가기
  • soldonii's devlog
Javascript 공부/React

리액트 useState() Hook : 함수형 컴포넌트로 state 관리하기

by soldonii 2019. 12. 5.

*Udemy의 "React - The Complete Guide" 강의에서 학습한 내용을 정리한 포스팅입니다.

*자바스크립트와 리액트를 배우는 단계라 오류가 있을 수 있습니다. 틀린 내용은 댓글로 말씀해주시면 수정하겠습니다. 감사합니다. :)


1. useState() 메소드로 state 관리 및 조작하기

최초 리액트가 등장했을 때는 state를 다룰 필요가 있을 경우 class 기반으로 컴포넌트를 만들고, state 조작이 필요없는 경우에 함수형 컴포넌트로 만드는 방식이 표준이었다. 그러나 리액트가 업데이트 되면서 함수형 컴포넌트에서도 state를 관리할 수 있게 진화했다.(그러나 강의가 촬영된 시점을 기준으로, 여전히 실무에서는 class 컴포넌트로 state를 관리하는게 대체로 표준이라고 한다.)

 

기존에 클래스 기반 컴포넌트의 코드는 아래와 같다.

import React, { Component } from 'react';
import './App.css';
import Person from './Person/Person';

class App extends Component {
  state = {
    persons: [
      { name: 'Max', age: 28 },
      { name: 'Manu', age: 29 },
      { name: 'Stephanie', age: 26 }
    ],
    otherState: 'some'
  }

  switchNameHandler = () => {
    this.setState({
      persons: [
        { name: 'Max', age: 28 },
        { name: 'Manu', age: 29 },
        { name: 'hyunsol', age: 31 }
      ]
    });
  }

  render() {
    return (
      <div className="App">
        <h1>Hi, I'm a React App</h1>
        <p>This is really working!</p>
        <button onClick={this.switchNameHandler}>Switch Name</button>
        <Person name={this.state.persons[0].name} age={this.state.persons[0].age} />
        <Person name={this.state.persons[1].name} age={this.state.persons[1].age} />
        <Person name={this.state.persons[2].name} age={this.state.persons[2].age} />
      </div>
    );
  }
}

클래스 컴포넌트를 함수형 컴포넌트로 변경시킬 때에는 더 이상 render() 메소드는 필요하지 않다. 그리고 useState() 메소드를 사용해서 state를 관리한다. 이 useState() 메소드는 항상 길이가 2인 배열을 리턴시킨다.

리턴되는 배열의 첫번째 element는 useState()에 메소드에 첫번째 인자로 전달된 state이고, 두번째 element는 state를 변경시키는 함수이다. 코드로 살펴보자.

 

import React, { useState } from 'react'; // Component 대신 use State
import './App.css';
import Person from './Person/Person';

const App = props => {
  const [ personsState, setPersonsState ] = useState({
    persons: [
      { name: 'Max', age: 28 },
      { name: 'Manu', age: 29 },
      { name: 'Stephanie', age: 26 }
    ],
    otherState: 'some'
  }); // useState 내부에 원래 state에 해당되는 데이터를 전달한다.

  console.log(personsState);

  const switchNameHandler = () => {
    setPersonsState({ // this.state 대신 setPersonsState
      persons: [
        { name: 'Max', age: 28 },
        { name: 'Manu', age: 29 },
        { name: 'hyunsol', age: 31 }
      ]
    });
  }

    return (
      <div className="App">
        <h1>Hi, I'm a React App</h1>
        <p>This is really working!</p>
        <button onClick={switchNameHandler}>Switch Name</button>
        <Person name={personsState.persons[0].name} age={personsState.persons[0].age} />
        <Person name={personsState.persons[1].name} age={personsState.persons[1].age} />
        <Person name={personsState.persons[2].name} age={personsState.persons[2].age} />
      </div>
    );
}

export default App;

1. 맨 윗줄에 import 시키는 대상이 Component에서 useState로 변경되었다.

2. useState() 메소드의 첫번째 인자로 다루고자 하는 대상이 되는 state를 전달했고, 전달받은 인자를 토대로 리턴되는 element들을 비구조화를 통해서 각각 personState 변수와 setPersonState 변수에 담아 두었다.

  - 즉 personState는 useState()에 첫번째 인자로 전달된 state가 담겨있고,

  - setPersonState는 personState의 값을 변경시킬 함수가 담겨있다.

3. switchNameHandler 내부에 기존의 this.state 대신 setPersonState 함수를 넣고, 함수의 인자로는 변경시키길 원하는 state를 전달한다.

 

클래스 기반의 컴포넌트와 함수형 컴포넌트에서 state를 다룰 때 대부분 같은 기능을 하지만 매우 중요한 차이점이 있다.

- 클래스 기반 컴포넌트 : 기존 state와 변경된 state를 병합(merge)한다. 
- 함수 기반 컴포넌트 : 기존 state를 변경된 state로 대치(replace)한다.

 

# 클래스 기반 컴포넌트

클래스 기반 컴포넌트에서 기존 최초의 state는 아래와 같았다.

// setState 실행 전 최초 state
state = {
  persons: [
    { name: 'Max', age: 28 },
    { name: 'Manu', age: 29 },
    { name: 'Stephanie', age: 26 }
  ],
  otherState: 'some'
}

state 내부에 persons 배열과 otherState라는 값 2개가 있다. 그리고 버튼을 클릭했을 때 persons 배열의 마지막 객체의 값을 { name: 'Stephanie', age: 26 } 에서 { name: 'hyunsol', age: 29 }로 변경하도록 this.setState 메소드를 실행했었다. setState 메소드 실행 후 변경된 state의 값은 아래와 같다.

// setState 실행 후 변경된 state
state = {
  persons: [
    { name: 'Max', age: 28 },
    { name: 'Manu', age: 29 },
    { name: 'hyunsol', age: 29 }
  ],
  otherState: 'some'
}

즉, state 내의 persons 배열과 otherState에서 update 시키는 person 내의 마지막 객체만 값이 변경되고, 나머지 전체 state는 고유하게 유지되어 있다. 변경시킨 부분만 기존 state와 '병합(merge)' 시켰기 때문이다.

# 함수 기반 컴포넌트

그러나 함수 기반 컴포넌트에서는 기존 state를 대치(replace) 시킨다고 했다.

// setPersonsState 실행 전 state
state = {
  persons: [
    { name: 'Max', age: 28 },
    { name: 'Manu', age: 29 },
    { name: 'Stephanie', age: 26 }
  ],
  otherState: 'some'
}

// setPersonsState 실행 후 state
state = {
  persons: [
    { name: 'Max', age: 28 },
    { name: 'Manu', age: 29 },
    { name: 'hyunsol', age: 29 }
  ]
}

setPersonsState 함수 실행 후 state 내부에서 otherState 데이터는 사라졌다. 왜냐하면 setPersonsState 함수에 인자로 전달된 state에서 otherState를 명시해주지 않았기 때문이다. 따라서 setPersonsState에 전달된 인자로 전체 state가 '대치(replace)'된 것이다.

 

# 해결책

이를 해결하기 위해서는 아래처럼 수동으로 기존의 원본 데이터의 값을 setPersonsState의 인자에 추가해서 넣어줄 수 있다.

const switchNameHandler = () => {
  setPersonsState({ // this.state 대신 setPersonsState
    persons: [
      { name: 'Max', age: 28 },
      { name: 'Manu', age: 29 },
      { name: 'hyunsol', age: 31 }
    ],
    otherState: personsState.otherState // 이 부분을 추가해야 한다.
  });
}

그러나 매번 이렇게 수동으로 코드를 추가하는 것은 에러의 발생 가능성을 높이고 효율성이 떨어진다. 다른 방법은 없을까?

 

클래스 기반의 컴포넌트에서 this.setState 메소드는 한 번만 사용한다. 즉 하나의 컴포넌트 내에서는 특정 조건이 충족되었을 때 state를 한 번만 변경시키는 것이다. 그러나 함수형 컴포넌트에서의 useState는 횟수에 상관없이 여러 차례 사용이 가능하다. 즉 원본 state를 여러개로 쪼개서 원하는 만큼 useState를 사용할 수 있는 것이다.

 

import React, { useState } from 'react'; // Component 대신 use State
import './App.css';
import Person from './Person/Person';

const App = props => {
  const [ personsState, setPersonsState ] = useState({
    persons: [
      { name: 'Max', age: 28 },
      { name: 'Manu', age: 29 },
      { name: 'Stephanie', age: 26 }
    ],
    otherState: 'some'
  }); // useState 내부에 원래 state에 해당되는 데이터를 전달한다.

  const [ otherState, setOtherState ] = useState({otherState: 'some'}); // otherState에 따로 state를 보관한다.
  console.log(personsState, otherState);

  const switchNameHandler = () => {
    setPersonsState({ // this.state 대신 setPersonsState
      persons: [
        { name: 'Max', age: 28 },
        { name: 'Manu', age: 29 },
        { name: 'hyunsol', age: 31 }
      ]
    });
  }

    return (
      <div className="App">
        <h1>Hi, I'm a React App</h1>
        <p>This is really working!</p>
        <button onClick={switchNameHandler}>Switch Name</button>
        <Person name={personsState.persons[0].name} age={personsState.persons[0].age} />
        <Person name={personsState.persons[1].name} age={personsState.persons[1].age} />
        <Person name={personsState.persons[2].name} age={personsState.persons[2].age} />
      </div>
    );
}

export default App;

serPersonsState 함수는 전체 state에서 personsState만 관리하고, setOthersState 함수가 othersState를 관리하도록 변경했다. 이렇게 따로 state를 관리하면 원본 state를 병합하는 대신 대치해서 발생하는 문제를 예방할 수 있다. 또한 하나의 컴포넌트 내에서 useState를 여러차례 이용해서 필요할 때마다 state를 변경시킬 수 있다는 장점도 있다.

 

2. Stateful vs. Stateless

# Stateful

컴포넌트 내부에서 state를 관리하는 컴포넌트를 stateful하다고 한다. 리액트로 앱을 만들 때 주의해야 할 점은, stateful한 컴포넌트는 하나의 앱에서 최소한으로 사용해야 한다는 점이다. 모든 컴포넌트가 제각기 state를 관리할 경우, 무척이나 복잡해지고 오류 디버깅이 불가능해진다. stateful한 컴포넌트는 smart 또는 container 컴포넌트라고도 부른다.

 

# Stateless

반대로 컴포넌트 내부에서 state를 관리하지 않는 컴포넌트를 stateless한 컴포넌트라고 부른다. 한 두개의 stateful 한 컴포넌트를 제외한 나머지 컴포넌트들은 거의 다 stateless한 컴포넌트로 만들어야 한다. stateless한 컴포넌트는 dumb 또는 presentational 컴포넌트라고도 부른다.

 

댓글