관리 메뉴

거니의 velog

(10) 컴포넌트 3 본문

React_리액트 시작

(10) 컴포넌트 3

Unlimited00 2023. 11. 29. 20:04

4. state

* 리액트에서 state는 컴포넌트 내부에서 바뀔 수 있는 값을 의미한다. props는 컴포넌트가 사용되는 과정에서 부모 컴포넌트가 설정하는 값이며, 컴포넌트 자신은 해당 props를 읽기 전용으로만 사용할 수 있다. props를 바꾸려면 부모 컴포넌트에서 바꾸어 주어야 한다.
  예를 들어 현재 상황에서는 App 컴포넌트에서 MyComponent를 사용할 때 props를 바꾸어 주어야 값이 변경될 수 있는 것이다. 반면 MyComponent에서는 전달받은 name 값을 직접 바꿀 수 없다.

* 리액트에서는 두 가지 종류의 state가 있다. 하나는 클래스형 컴포넌트가 지니고 있는 state이고, 다른 하나는 함수형 컴포넌트에서 useState라는 함수를 통해 사용하는 state이다.


(1) 클래스형 컴포넌트의 state

* 새로운 컴포넌트를 만들어 주자. Counter.js 파일을 src 디렉터리에 생성하여 다음 코드를 작성해 보자.

import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    // state의 초깃값 설정하기
    this.state = {
      number: 0,
    };
  }
  render() {
    const { number } = this.state; // state를 조회할 때는 this.state로 조회한다.
    return (
      <div>
        <h1>{number}</h1>
        <button
          // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정한다.
          onClick={() => {
            // this.setState를 사용하여 state에 새로운 값을 넣을 수 있다.
            this.setState({ number: number + 1 });
          }}
        >
          +1
        </button>
      </div>
    );
  }
}

export default Counter;

* 위 파일에서 각 코드가 어떤 역할을 하는지 알아보자.

* 컴포넌트에 state를 설정할 때는 다음과 같이 constructor 메서드를 작성하여 설정한다.

  constructor(props) {
    super(props);
    // state의 초깃값 설정하기
    this.state = {
      number: 0,
    };
  }

* 이는 컴포넌트의 생성자 메서드인데, 클래스형 컴포넌트에서 constructor를 작성할 때는 반드시 super(props)를 호출해 주어야 한다. 이 함수가 호출되면 현재 클래스형 컴포넌트가 상속받고 있는 리액트의 Component 클래스가 지닌 생성자 함수를 호출해 준다.

* 그 다음에 this.state 값에 초깃값을 설정해 주었다. 컴포넌트의 state는 객체 형식이어야 한다.

* 이제 render 함수를 확인해 보자.

  render() {
    const { number } = this.state; // state를 조회할 때는 this.state로 조회한다.
    return (
      <div>
        <h1>{number}</h1>
        <button
          // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정한다.
          onClick={() => {
            // this.setState를 사용하여 state에 새로운 값을 넣을 수 있다.
            this.setState({ number: number + 1 });
          }}
        >
          +1
        </button>
      </div>
    );
  }

* render 함수에서 현재 state를 조회할 때는 this.state를 조회하면 된다. 그리고 button 안에 onClick이라는 값을 props로 넣어 주었는데, 이는 버튼이 클릭될 때 호출시킬 함수를 설정할 수 있게 해준다. 이를 이벤트를 설정한다고 하는데, 리액트의 이벤트 시스템은 나중에 더 자세히 알아보자.

* 이벤트로 설정할 함수를 넣어줄 때는 화살표 함수 문법을 사용하여 넣어 주어야 한다. 함수 내부에서는 this.setState라는 함수를 사용하는데, 이 함수가 state 값을 바꿀 수 있게 해준다.

* 코드를 다 작성했으면 Counter 컴포넌트를 App에서 불러와 렌더링하자. 기존 MyComponent는 이제 필요 없으니 없애 주자.

import React from 'react';
import Counter from './Counter';

const App = () => {
  return <Counter />;
};

export default App;

* 브라우저에서 다음과 같이 숫자와 버튼이 나타날 것이다. 버튼을 누르면 숫자가 1씩 잘 올라가는 모습을 볼 수 있다.

[1] state 객체 안에 여러 값이 있을 때

* state 객체 안에는 여러 값이 있을 수 있다. Counter 컴포넌트를 다음과 같이 한 번 수정해 보자.

import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    // state의 초깃값 설정하기
    this.state = {
      number: 0,
      fixedNumber: 0,
    };
  }
  render() {
    const { number, fixedNumber } = this.state; // state를 조회할 때는 this.state로 조회한다.
    return (
      <div>
        <h1>{number}</h1>
        <h2>바뀌지 않는 값: {fixedNumber}</h2>
        <button
          // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정한다.
          onClick={() => {
            // this.setState를 사용하여 state에 새로운 값을 넣을 수 있다.
            this.setState({ number: number + 1 });
          }}
        >
          +1
        </button>
      </div>
    );
  }
}

export default Counter;

* 현재 state 안에 fixedNumber라는 또 다른 값을 추가해 주었다. 버튼이 클릭될 때 fixedNumber 값은 그대로 두고 number 값만 바꿀 것인데, 그렇다고 해서 this.setState 함수를 사용할 때 인자로 전달되는 객체 내부에 fixedNumber를 넣어 주지는 않았다. this.setState 함수는 인자로 전달된 객체 안에 들어 있는 값만 바꾸어 준다.

* 코드로 저장하고 브라우저를 열어서 버튼을 눌러 보자. 맨 위에 있는 숫자만 업데이트되고 하단의 숫자는 잘 고정되어 있는가?

[2] state 를 constructor 에서 꺼내기

* 앞에서 state의 초깃값을 지정하기 위해 constructor 메서드를 선언해 주었는데, 또 다른 방식으로도 state의 초깃값을 지정해 줄 수 있다.

* 코드를 다음과 같이 수정해 보자.

import React, { Component } from 'react';

class Counter extends Component {
  state = {
    number: 0,
    fixedNumber: 0,
  };
  render() {
    const { number, fixedNumber } = this.state; // state를 조회할 때는 this.state로 조회한다.
    return (
      <div>
        <h1>{number}</h1>
        <h2>바뀌지 않는 값: {fixedNumber}</h2>
        <button
          // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정한다.
          onClick={() => {
            // this.setState를 사용하여 state에 새로운 값을 넣을 수 있다.
            this.setState({ number: number + 1 });
          }}
        >
          +1
        </button>
      </div>
    );
  }
}

export default Counter;

* 이렇게 하면 constructor 메서드를 선언하지 않고도 state 초깃값을 설정할 수 있다. 우리는 앞으로 state 를 사용할 때 이 방식을 사용하여 state의 초깃값을 설정할 것이다.

[3] this.setState에 객체 대신 함수 인자 전달하기

* this.setState를 사용하여 state 값을 업데이트할 때는 상태가 비동기적으로 업데이트된다. 만약 다음과 같이 onClick에 설정한 함수 내부에서 this.setState 를 두 번 호출하면 어떻게 될까?

import React, { Component } from 'react';

class Counter extends Component {
  state = {
    number: 0,
    fixedNumber: 0,
  };
  render() {
    const { number, fixedNumber } = this.state; // state를 조회할 때는 this.state로 조회한다.
    return (
      <div>
        <h1>{number}</h1>
        <h2>바뀌지 않는 값: {fixedNumber}</h2>
        <button
          // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정한다.
          onClick={() => {
            // this.setState를 사용하여 state에 새로운 값을 넣을 수 있다.
            this.setState({ number: number + 1 });
            this.setState({ number: this.state.number + 1 });
          }}
        >
          +1
        </button>
      </div>
    );
  }
}

export default Counter;

* 코드를 위와 같이 작성하면 this.setState를 두 번 사용하는 것임에도 불구하고 버튼을 클릭할 때 숫자가 1씩 더해진다.  this.setState를 사용한다고 해서 state 값이 바로 바뀌지는 않기 때문이다.

* 이에 대한 해결책은 this.setState를 사용할 때 객체 대신에 함수를 인자로 넣어주는 것이다. this.setState의 인자로 함수를 넣어 줄 때는 코드를 다음과 같은 형식으로 작성한다.

this.setState((prevState, props) => {
	return {
    	// 업데이트하고 싶은 내용
    }
})

* 여기서 prevState는 기존 상태이고, props는 현재 지니고 있는 props를 가리킨다. 만약 업데이트하는 과정에서 props가 필요하지 않다면 생략해도 된다.

* 기존 코드를 다음과 같이 작성해 보자.

import React, { Component } from 'react';

class Counter extends Component {
  state = {
    number: 0,
    fixedNumber: 0,
  };
  render() {
    const { number, fixedNumber } = this.state; // state를 조회할 때는 this.state로 조회한다.
    return (
      <div>
        <h1>{number}</h1>
        <h2>바뀌지 않는 값: {fixedNumber}</h2>
        <button
          // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정한다.
          onClick={() => {
            // this.setState를 사용하여 state에 새로운 값을 넣을 수 있다.
            this.setState((prevState) => {
              return {
                number: prevState.number + 1,
              };
            });
            // 위 코드와 아래 코드는 완전히 똑같은 기능을 한다.
            // 아래 코드는 함수에서 바로 객체를 반환한다는 의미이다.
            this.setState((prevState) => ({
              number: prevState.number + 1,
            }));
          }}
        >
          +1
        </button>
      </div>
    );
  }
}

export default Counter;

* 화살표 함수에서 값을 바로 반환하고 싶다면 코드 블록 { }을 생략하면 된다. 예를 들어, 파라미터 a와 b를 받아 와서 합을 구해 주는 함수를 작성하고 싶다면 다음과 같이 작성할 수 있다.

const sum = (a, b) => a + b;

* onClick에서 두 번째로 this.setState 함수를 사용할 때는 화살표 함수에서 바로 객체를 반환하도록 했기 때문에 prevState => ({  })와 같은 형태로 코드가 이루어 진다.

* 브라우저에서 버튼을 눌러 보면, 숫자가 2씩 올라가는 모습을 볼 수 있다.

[4] this.setState가 끝난 후 특정 작업 실행하기

* setState를 사용하여 값을 업데이트하고 난 다음에 특정 작업을 하고 싶을 때는 setState의 두 번째 파라미터로 콜백(callback) 함수를 등록하여 작업을 처리할 수 있다.

* onClick 함수를 다음과 같이 수정해 보자.

import React, { Component } from 'react';

class Counter extends Component {
  state = {
    number: 0,
    fixedNumber: 0,
  };
  render() {
    const { number, fixedNumber } = this.state; // state를 조회할 때는 this.state로 조회한다.
    return (
      <div>
        <h1>{number}</h1>
        <h2>바뀌지 않는 값: {fixedNumber}</h2>
        <button
          // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정한다.
          onClick={() => {
            // this.setState를 사용하여 state에 새로운 값을 넣을 수 있다.
            this.setState(
              {
                number: number + 1,
              },
              () => {
                console.log('방금 setState가 호출되었습니다.');
                console.log(this.state);
              }
            );
            // 위 코드와 아래 코드는 완전히 똑같은 기능을 한다.
            // 아래 코드는 함수에서 바로 객체를 반환한다는 의미이다.
            this.setState((prevState) => ({
              number: prevState.number + 1,
            }));
          }}
        >
          +1
        </button>
      </div>
    );
  }
}

export default Counter;

* 이렇게 콜백 함수를 등록한 뒤 브라우저를 열어서 버튼을 누르고 개발자 도구의 Console 탭을 확인해 보자.


(2) 함수형 컴포넌트에서 useState 사용하기

* 리액트 16.8 이전 버전에서는 함수형 컴포넌트에서 state를 사용할 수 없었다. 하지만 16.8 이후부터는 useState라는 함수를 사용하여 함수형 컴포넌트에서도 state를 사용할 수 있게 되었다. 사용법은 조금 다르다.

* 이 과정에서 Hooks라는 것을 사용하게 되는데, Hooks의 종류는 다양하지만, 이번에는 useState만 배워 보고 나머지는 이후에 공부해 보자.

[1] 배열 비구조화 할당

* Hooks를 사용하기 전에 배열 비구조화 할당이라는 것을 알아보자. 배열 비구조화 할당은 이전에 배운 객체 비구조화 할당과 비슷하다. 즉, 배열 안에 들어 있는 값을 쉽게 추출할 수 있도록 해 주는 문법이다.

* 다음 코드를 한 번 확인해 보자.

const array = [1, 2];
const one = array[0];
const two = array[1];

* array 안에 있는 값을 one 과 two 에 담아 주는 코드인데, 위 코드는 배열 비구조화 할당을 사용하면 다음과 같이 표현할 수 있다.

const array = [1, 2];
const [one, two] = array;

* 훨씬 코드가 깔끔해진다.

[2] useState 사용하기

* 배열 비구조화 할당 문법을 알고 나면 useState 사용 방법을 쉽게 이해할 수 있다. 새 컴포넌트를 만들어서 useState를 사용해 보자. src 디렉터리에 Say.js라는 파일을 생성하고 다음 코드를 작성해 보자.

import React, { useState } from 'react';

const Say = () => {
  const [message, setMessage] = useState('');
  const onClickEnter = () => setMessage('안녕하세요!');
  const onClickLeave = () => setMessage('안녕히 가세요!');
  return (
    <div>
      <button onClick={onClickEnter}>입장</button>
      <button onClick={onClickLeave}>퇴장</button>
      <h1>{message}</h1>
    </div>
  );
};

export default Say;

* useState 함수의 인자에는 상태의 초깃값을 넣어 준다. 클래스형 컴포넌트에서의 state 초깃값은 객체 형태를 넣어 주어야 한다고 배웠는데, useState에서는 반드시 객체가 아니어도 상관없다. 값의 형태는 자유이다. 숫자일 수도, 문자열일 수도, 객체일 수도, 배열일 수도 있다.

* 함수를 호출하면 배열이 반환되는데, 배열의 첫 번째 원소는 현재 상태이고, 두 번째 원소는 상태를 바꾸어 주는 함수이다. 이 함수를 세터(Setter) 함수라고 부른다. 그리고 배열 비구조화 할당을 통해 이름을 자유롭게 정해줄 수 있다. 현재 message와 setMessage라고 이름을 설정해 주었는데, text와 setText라고 이름을 자유롭게 바꾸어 주어도 상관 없다.

* 한번 Say 컴포넌트를 App에서 렌더링해 보고 입장 버튼과 퇴장 버튼을 눌러 보자.

import React from 'react';
import Say from './Say';

const App = () => {
  return <Say />;
};

export default App;

[3] 한 컴포넌트에서 useState 여러 번 사용하기

* useState는 한 컴포넌트에서 여러 번 사용해도 상관없다. 또 다른 상태를 useState로 한번 관리해 보자.

import React, { useState } from 'react';

const Say = () => {
  const [message, setMessage] = useState('');
  const onClickEnter = () => setMessage('안녕하세요!');
  const onClickLeave = () => setMessage('안녕히 가세요!');

  const [color, setColor] = useState('black');

  return (
    <div>
      <button onClick={onClickEnter}>입장</button>
      <button onClick={onClickLeave}>퇴장</button>
      <h1 style={{ color }}>{message}</h1>
      <button style={{ color: 'red' }} onClick={() => setColor('red')}>
        빨간색
      </button>
      <button style={{ color: 'green' }} onClick={() => setColor('green')}>
        초록색
      </button>
      <button style={{ color: 'blue' }} onClick={() => setColor('blue')}>
        파란색
      </button>
    </div>
  );
};

export default Say;

* 코드를 저장하고 입장 버튼을 눌러서 텍스트를 띄워 보자. 그리고 색상이 표시되어 있는 버튼을 눌러 보자. 텍스트 색상이 잘 바뀌는가?


5. state를 사용할 때 주의 사항

* 클래스형 컴포넌트든 함수형 컴포넌트든 state를 사용할 때는 주의해야 할 사항이 있다. state 값을 바꾸어야 할 때는 setState 혹은 useState를 통해 전달받은 세터 함수를 사용해야 한다.

* 예를 들어 다음 코드는 잘못된 코드이다.

// 클래스형 컴포넌트에서...
this.state.number = this.state.number + 1;
this.state.array = this.array.push(2);
this.state.object.value = 5;

// 함수형 컴포넌트에서...
const [object, setObject] = useState({ a: 1, b: 1 });
object.b = 2;

* 그렇다면 배열이나 객체를 업데이트해야 할 때는 어떻게 해야 할까? 이런 상황에서는 배열이나 객체 사본을 만들고 그 사본에 값을 업데이트한 후, 그 사본의 형태를 setState 혹은 세터 함수를 통해 업데이트한다.

* 사본을 만들어서 업데이트하는 예시는 다음과 같다.

// 객체 다루기
const object = { a: 1, b: 2, c: 3 };
const nextObject = { ...object, b: 2 }; // 사본을 만들어서 b 값만 덮어 쓰기

// 배열 다루기
const array = [
    { id: 1, value: true },
    { id: 2, value: true },
    { id: 3, value: false }
];
let nextArray = array.concat({ id: 4 }); // 새 항목 추가
nextArray.filter(item => item.id !== 2); // id가 2인 항목 제거
nextArray.map(item => (item.id === 1 ? { ...item, value: false } : item)); // id가 1인 항목의 value를 false로 설정

* 객체에 대한 사본을 만들 때는 spread 연산자라 불리는 ...을 사용하여 처리하고, 배열에 대한 사본을 만들 때는 배열의 내장 함수들을 활용한다. 이에 대한 자세한 내용은 이후에 차근차근 살펴보자.


6. 정리

* 이 장에서는 컴포넌트를 만들어서 내보내고 불러오는 방법과 props 및 state를 사용하는 방법을 배워 보았다. props 와 states는 둘 다 컴포넌트에서 사용하거나 렌더링할 데이터를 담고 있으므로 비슷해 보일 수 있지만, 그 역할은 매우 다르다. props는 부모 컴포넌트가 설정하고, state는 컴포넌트 자체적으로 지닌 값으로 컴포넌트 내부에서 값을 업데이트 할 수 있다.

* props를 사용한다고 해서 값이 무조건 고정적이지는 않는다. 부모 컴포넌트의 state를 자식 컴포넌트의 props로 전달하고, 자식 컴포넌트에서 특정 이벤트가 발생할 때 부모 컴포넌트의 메서드를 호출하면 props도 유동적으로 사용할 수 있다. 이후 만들어 볼 일정 관리 애플리케이션에서 이러한 구조로 프로젝트를 설계하게 된다.

* 이 장에서는 state를 다루기 위해 클래스형 컴포넌트의 state와 함수형 컴포넌트의 useState에 대해 배워 보았다. 앞으로 새로운 컴포넌트를 만들 때는 useState를 사용할 것을 권장한다. 이로써 코드가 더 간결해질 뿐만 아니라, 리액트 개발 팀이 함수형 컴포넌트와 Hooks를 사용하는 것이 주요 컴포넌트 개발 방식이 될 것이라고 공지했기 때문이다.

* 단, 초반부에는 클래스 컴포넌트의 사용법도 알아보기 위해 클래스형 컴포넌트의 state도 사용할 것이다.

'React_리액트 시작' 카테고리의 다른 글

(12) 이벤트 핸들링 2  (0) 2023.11.30
(11) 이벤트 핸들링 1  (2) 2023.11.30
(9) 컴포넌트 2  (0) 2023.11.29
(8) 컴포넌트 1  (0) 2023.11.29
(7) JSX 4  (0) 2023.11.29