[React] 리액트 state
- 리액트에서 state는 컴포넌트 내부에서 바뀔 수 있는 값을 의미한다.
- props는 컴포넌트가 사용되는 과정에서 부모 컴포넌트가 설정하는 값이며, 컴포넌트 자신은 해당 props를 읽기 전용으로만 사용할 수 있다.
- props를 바꾸려면 부모 컴포넌트에서 바꿔 줘야 한다.
- 리액트에는 두 가지 종류의 state가 있다.
- 하나는 클래스형 컴포넌트가 지니고 있는 state이고, 다른 하나는 함수 컴포넌트에서 useState라는 함수를 통해 사용하는 state 이다.
클래스형 컴포넌트의 state
- 새로운 컴포넌트를 만들자. Counter.js 파일을 src 디렉터리에 생성하여 다음 코드를 작성해 보자.
* Counter.js
import { 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에서 불러와 렌더링하도록 한다.
* App.js
import Counter from './Counter'; const App = () => { return ( <Counter /> ); } export default App; |
- 브라우저에서 숫자와 버튼이 나타났는가? 버튼을 눌러 보면 숫자가 1씩 올라갈 것이다.
state 객체 안에 여러 값이 있을 때
- state 객체 안에는 여러 값이 있을 수 있다. Counter 컴포넌트를 다음과 같이 수정해 보자.
* Counter.js
import { 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 함수는 인자로 전달된 개체 안에 들어 있는 값만 바꾸어 준다.
- 코드를 저장하고 브라우저를 열어서 버튼을 눌러 보자. 맨 위에 있는 숫자만 업데이트되고 하단의 숫자는 고정 돼 있을 것이다.
state를 constructor에서 꺼내기
- 앞에서 state의 초깃값을 지정하기 위해 constructor 메서드를 선언해 주었는데, 또 다른 방식으로도 state의 초기값을 지정해 줄 수 있다.
- 코드를 다음과 같이 수정한다.
* Counter.js
import { 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 초기값을 설정할 수 있다.
this.setState에 객체 대신 함수 인자 전달하기
- this.setState를 사용해 state 값을 업데이트할 때는 상태가 비동기적으로 업데이트된다.
- 만약 다음과 같이 onClick에 설정한 함수 내부에서 this.setState를 두 번 호출하면 어떻게 될까?
* Counter.js
<button // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정한다. onClick={()=>{ // this.setState를 사용해 state에 새로운 값을 넣을 수 있다. this.setState({ number: number + 1}); this.setState({ number: this.state.number + 1}); }}>+1</button> |
- 코드를 위와 같이 작성하면 this.setState를 두 번 사용하는 것임에도 불구하고 버튼을 클릭할 때 숫자가 1씩 더해진다.
- this.setState를 사용한다고 해서 state 값이 바로 바뀌지는 않기 때문이다.
- 위에 대한 해결책은 this.setState를 사용할 때 객체 대신에 함수를 인자로 넣어 주는 것이다.
- this.setState의 인자로 함수를 넣어 줄 때는 코드를 다음과 같은 형식으로 작성한다.
this.setState((prevState, props) => { return { // 업데이트할 내용 } }) |
- 여기서 prevState는 기존 상태이고, props는 현재 지니고 있는 props를 가리킨다.
- 만약 업데이트 과정에서 props가 필요하지 않다면 생략해도 된다.
- 기존 코드를 다음과 같이 작성한다.
* Counter.js - button
<button // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정한다. onClick={()=>{ this.setState(prevState => { return { number: prevState.number + 1 }; }); // 위 코드와 아래 코드는 완전히 똑같은 기능을 한다. // 아래 코드는 함수에서 바로 객체를 반환한다는 의미다. this.setState(prevState =>({ number: prevState.number + 1 })); }} >+1</button> |
- 화살표 함수에서 값을 바로 반환하고 싶으면 코드 블록 { }를 생략하면 된다.
- 예를 들어, 파라미터 a와 b를 받아 와서 합을 구해 주는 함수를 작성하고 싶다면 다음과 같이 작성할 수 있다.
const sum = (a, b) => a + b;
- onClick에서 두 번째로 this.setState 함수를 사용할 때는 화살표 함수에서 바로 객체를 반환하도록 했기 때문에 prevState => ({ })와 같은 형태로 코드가 이루어진다.
this.setState가 끝난 후 특정 작업 실행하기
- setState를 사용하여 값을 업데이트하고 난 다음에 특정 작업을 하고 싶을 때는 setState의 두 번째 파라미터로 콜백(callback) 함수를 등록하여 작업을 처리할 수 있다.
- onClick 함수를 다음과 같이 수정한다.
* Counter.js - button
<button // onClick을 통해 버튼이 클릭되었을 때 호출할 함수를 지정한다. onClick={()=>{ this.setState( { number: number + 1 }, () => { console.log('방금 setState가 호출되었습니다.'); console.log(this.state); } ); }}>+1</button> |
- 이렇게 콜백 함수를 등록한 뒤 브라우저를 열어서 버튼을 누르고 개발자 도구의 [Console] 탭을 확인해 보자.
함수 컴포넌트에서 useState 사용하기
- 리액트 16.8 이전 버전에서는 함수 컴포넌트에서 state를 사용할 수 없었지만, 이후 버전에서는 useState라는 함수를 사용하여 함수 컴포넌트에서도 state를 사용할 수 있게 되었다.
배열 비구조화 할당
- Hooks를 사용하기 전에 배열 비구조화 할당이라는 것을 알아보자.
- 배열 비구조화 할당은 객체 비구조화 할당과 비슷하며, 배열 안에 들어 있는 값을 쉽게 추출할 수 있도록 해 주는 문법이다.
- 다음 코드를 확인하자.
const array = [1, 2];
const one = array[0];
const two = array[1];
- array 안에 있는 값을 one과 two에 담아 주는 코드인데, 위 코드는 배열 비구조화 할당을 사용하면 다음과 같이 표현할 수 있다.
const array = [1, 2];
const [one, two] = array;
- 코드가 훨씬 깔끔하다.
useState 사용하기
- 배열 비구조화 할당 문법을 알고 나면 useState 사용 방법을 쉽게 이해할 수 있다.
- 새 컴포넌트를 만들어서 useState를 사용해 보자.
- src 디렉터리에 Say.js 파일을 생성하고 다음 코드를 작성한다.
* Say.js
import { 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에서는 반드시 객체가 아니어도 상관없다.
- 값의 형태는 자유이다. 숫자일 수도, 문자열일 수도, 배열일 수도 있다.
- useState( ) 함수를 호출하면 배열이 반환되는데, 배열의 첫 번째 원소는 현재 상태이고, 두 번째 원소는 상태를 바꾸어 주는 함수이다. 이 함수를 세터(Setter)라고 한다.
- 그리고 배열 비구조화 할당을 통해 이름을 자유롭게 정할 수 있다.
- 현재 message와 setMessage라고 이름을 설정했는데, text와 setText라고 이름을 자유롭게 바꾸어 주어도 상관없다.
- Say 컴포넌트를 App에서 렌더링해 보고 [입장] 버튼과 [퇴장] 버튼을 눌러 보자.
* App.js
import Say from './Say'; const App = () => { return ( <Say /> ); } export default App; |
한 컴포넌트에서 useState 여러 번 사용하기
- useState는 여러 번 사용해도 상관없다. 또 다른 상태를 useState로 관리해 보자.
* Say.js
import { 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; |
- 코드를 저장하고 [입장] 버튼을 눌러서 텍스트를 띄워 보자. 그리고 색상이 표시되어 있는 버튼을 눌러 보자. 텍스트 색상이 잘 바뀌는가?
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 연산자라 불리는 ...을 사용하여 처리하고, 배열에 대한 사본을 만들 때는 배열의 내장 함수들을 활용한다.