리액트/TicTacToe

TICTACTOE 만들기 3 (State, Super, 불변성)

워제하 2024. 7. 31. 17:36

 

- 리액트에서 데이터가 변할 때 화면을 다시 렌더링 해주기 위해서는 React State를 사용해야한다.

 

● React State란 ? 

 

컴포넌트의 렌더링 결과물에 영향을 주는 데이터를 갖고 있는 객체이다.

State가 변경되면 컴포넌트는 리랜더링(Re-rendering)된다. 또한, State는 컴포넌트 안에서 관리된다.

 

 

● State 추가하기

컴포넌트 안에서 무언가를 기억하기 위해서 state를 사용한다.

state를 생성할 때는 constructor 안에서 생성해주면 된다.

 

 

 

● Constructor

constructor(생성자)를 사용하면 인스턴스화된 객체에서 다른 메서드를 호출하기 전에 수행해야 하는 사용자 지정 초기화를 제공할 수 있다.

 

클래스를 new를 붙여서 ( new User("John")) 인스턴스 객체로 생성하면 넘겨받은 인수와 함께 constructor가 먼저 실행된다.

이때 넘겨받은 인수인 John이 this.name에 할당된다.

class User {
	constructor(name) {
    	this.name = name;
    }
    
    sayHi() {
    	alert(this.name);
    }
}

let User = new User("John");
user.sayHi();

 

 

 

 

 

< 실습하기 >

State 생성할 때 value 값을 null로 지정했기 때문에 setState를 사용해서 value의 값을 X로 변경해주고 변경한 value 값을 이용해주는 코드를 작성한다.

 

 

※ 여기서 주의해야할 점은

JavaScript 클래스에서 하위 클래스의 생성자를 정의할 때 항상 super를 호출해야한다. 이유는 모든 React 컴포넌트 클래스는 생성자를 가질 때 super(props) 호출 구문부터 작성해야하기 때문이다.

 

 

 

실행시키면 클릭했을 때 해당 square 부분에 x 표시가 뜨게 된다. (지금 스타일링이 안맞지만 무시!)

 

 

 

 

 

 

● Super

- super 키워드는 자식 클래스 내에서 부모 클래스의 생성자(3번) 또는 메소드(6번)를 호출할 때 사용된다.

=> 결과값 : I have a Ford, it is a Mustang

 

 

<문법>

super([arguments]); //부모 생성자 호출

super.functionOnParent([arguments]);

 

 

 

 

Super 이후에 this 키워드

- 생성자에서는  super 키워드 하나만 사용되거나 this 키워드가 사용되기 전에 호출되어야 한다.

 

 

 

Super 이후에 this 키워드가 나와야 하는 이유

- 아래 소스 코드에서처럼 부모 클래스의 생성자를 호출하기 전에 this.name을 사용하려고 하면 문제가 되기 때문이다.

- React에서 this.state 을 생성자에서 정의할 때 super 가 먼저 와야하는 이유도 이와 같다.

문제 발생, super로 할당하지 않았는데 this.name을 사용했으므로

 

 

 

 

React에서 super에 props를 인자로 전달하는 이유

- React, Component 객체가 생성될 때 props 속성을 초기화하기 위해 부모 컴포넌트에게 props를 전달한다.

- 생성자 내부에서도 this.props를 정상적으로 사용할 수 있도록 보장하기 위해서이다.

 

: 여기서 2번째 단락의 코드인 2번째 constructor처럼 super() 안에 props를 넣어 주지 않아도 console.log에 pops가 찍히지만, 실제로 사용하기 위해서는 3번째 단락 코드인 3번째 constructor처럼 super() 안에 props를 넣어줘야 console.log(this.props) 했을 때 사용이 가능하다.

 

 

즉, 

 

( 실제 리액트를 사용할 때는

1. state를 정의해줄 때 constructor 안에 정의를 해주고,

2. this 키워드를 사용할 때는 this 앞에 꼭 super를 사용하고

3. super 안에 props를 넣어주면 된다

는 것만 알고 있으면 된다. )

 

 

 

 

 

 

● State  VS  Props

 

 

 

 

 

 

● 부모 컴포넌트에서 State 보관하기

여러 개의 자식으로부터 데이터를 모으거나 두 개의 자식 컴포넌트들이 서로 통신하게 하려면 부모 컴포넌트에 공유 state를 정의해야 한다. 부모 컴포넌트는 props를 사용하여 자식 컴포넌트에 state를 다시 전달할 수 있다. 이것은 자식 컴포넌트들이 서로 또는 부모 컴포넌트와 동기화하도록 만든다.

 

 

현재는 자식 컴포넌트인 square에서 state를 가지고 있는데 이 state를 부모 컴포넌트인 Board로 옮길 것이다. 

 

 

여기서 각각의 square에 들어가야 하는 값은 X, O, NULL 이렇게 3가지가 각각 다르게 들어가야하므로 배열을 만들어 줘야한다.

 

1) Board에 생성자를 추가하고 9개의  사각형에 해당하는 9개의 null 배열을 초기 state로 설정한다.

 [

   null, null, null

   null, null, null
   null, null, null

 ]

 

Board.js 에서 constructor(props)를 작성해준다.

 

여기서 Array.prototype.fill()을 사용했는데 

fill() 메서드는 배열의 시작 인덱스부터 끝 인덱스의 이전까지 정적인 값 하나로 채운다.

 

ex ) Array(9).fill(null)   => this.state = ,,,,,,,,

       Array(9).fill(2)   => this.state = 2,2,2,2,2,2,2,2,2

 

 

그리고 Square.js에서 constructor(props) 를 지워준다. 

왜냐하면 부모 컴포넌트인 Board.js로 옮겼기 때문이다.

 

 

 

이렇게 했으면 Board에서 작성한 Squares 를 Square.js에서 this.state.value에 사용하기위해 내려줘야한다.

renderSquare에 this.state.squares[i] 를 작성해주고

Square.js에서는 this.state.value 를 this.props.value로 변경해주면 된다.

.Board.js

 

 

Square.js

 

 

 

 

 

 

이번에는 Square 컴포넌트 클릭시 변화주는 코드를 작성해본다.

 

1. Square 클릭 시 호출할 함수를 생성한다. (handleClick 함수 생성)

 

여기서 this.state.squares가 

 [

   null, null, null

   null, null, null
   null, null, null

 ]

를 나타낸다. (원본 배열)

Array.prototype.slice() 에서 slice() 메서드는 어떤 배열의 begin 부터 end 까지(end 미포함)에 대한 얕은 복사본을 새로운 배열 객체로 반환하는 것으로 원본 배열을 바뀌지 않는다.

 

 

 

2. 해당 함수를 Props로 Square 컴포넌트에 전달한다.

 

 

 

3. 내려받은 Props를 위한 Square 컴포넌트를 변경한다.

- Square의 render 함수 내부의 this.state.value 를 this.props.value 로 바꾸어 주고

- Square의 render 함수 내부의 this.setState()를 this.props.onClick()으로 바꾸어준다.

- 그리고 Square는 게임의 상태를 유지할 필요가 없기 때문에 constructor를 지워주면 된다.

 

 

 

 

< 현재 Square 컴포넌트를 클릭할 때 발생하는 일들 >

1. 내장된 DOM <button> 컴포넌트에 있는 onClick prop은 React에게 클릭 이벤트 리스너를 설정하라고 알려준다.

2. 버튼을 클릭하면 React는 Square의  render() 함수에 정의된 onClick 이벤트 핸들러를 호출한다.

3. 이벤트 핸들러는 this.props.onClick() 를 호출한다. Square의 onClick prop은 Board에서 정의되었다.

4. Board에서 Square로 onClick={ () => this.handleClick(i) } 를 전달했기 때문에 Square를 클릭하면 Board의 handleClick(i)를 호출한다.

 

 

 

 

 

 

여기까지 하고 실행하면 클릭 시 X가 잘 들어가는 것을 확인 할 수 있다.

 

- Board.js 코드

import React, { Component } from 'react'
import Square from './Square'
import "./Board.css";

export default class Board extends Component {

  constructor(props) {
    super(props);
    this.state = {
      squares : Array(9).fill(null),
    };
  }

  handleClick(i){
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({ squares:squares});
  }

    renderSquare(i){
        return (
            <Square 
                value ={this.state.squares[i]} 
                onClick={()=> this.handleClick(i)}
            />
        )
    }

  render() {
    return (
      <div>
        <div className='status'>Next Palyer : X, O</div>
        <div className='board-row'>
            {this.renderSquare(0)}
            {this.renderSquare(1)}
            {this.renderSquare(2)}
        </div>
        <div className='board-row'>
            {this.renderSquare(3)}
            {this.renderSquare(4)}
            {this.renderSquare(5)}
        </div>
        <div className='board-row'>
            {this.renderSquare(6)}
            {this.renderSquare(7)}
            {this.renderSquare(8)}
        </div>
      </div>
    )
  }
}

 

 

- Square.js 코드

import React from "react";
import "./Square.css";

export default class Square extends React.Component {
    

    render() {
        return (
            <button 
                className="square"
                onClick={() => this.props.onClick()}
            >
                {this.props.value} 
            </button>
        )
    }
}

 

 

 

 

 

 

 

● 리액트의 불변성 지키기

리액트에서 불변성이란?

불변성이란 사전적 의미로는 값이나 상태를 변경할 수 없는 것을 의미한다.

 

자바스크립트 타입에서 원시 타입은 불변성(immutable)을 가지고 있고 참조 타입은 그렇지 않다(mutable).

- 원시 타입 : Boolean, String, Number, null, undefined, Symbol (불변성 가지고 있음)

- 참조 타입 : Object, Array 

 

 

 

기본적으로 JavaScript는

원시 타입에 대한 참조 및 값을 저장하기 위해 Call Stack 메모리 공간을 사용하지만

참조 타입의 경우 Heap 이라는 별도의 메모리 공간을 사용한다. 이 경우 Call Stack 은 개체 및 배열 값이 아닌 메모리에만 Heap 메모리 참조 ID를 값으로 저장한다.

 

 

 

 

 

● 불변성을 지켜야하는 이유?

1. 참조 타입에서 객체나 배열의 값이 변할 때 원본 데이터가 변경되기에 이 원본 데이터를 참조하고 있는 다른 객체에서 예상치 못한 오류가 발생할 수 있어서 프로그래밍의  복잡도가 올라간다.

2. 리액트에서 화면을 업데이트 할 때 불변성을 지켜서 값을 이전 값과 비교해서 변경된 사항을 확인한 후 업데이트하기 때문에 불변성을 지켜줘야 한다.

 

 

● 불변성을 지키는 방법은?

- 참조 타입에서는 값을 바꿨을 때 Call Stack 주소 값은 같은데 Heap 메모리 값만 바꿔주기에 불변성을 유지할 수 없었으므로 아예 새로운 배열을 반환하는 메소드를 사용하면 된다.

 - spread operator, map, filter, slice, reduce

- 원본 데이터를 변경하는 메소드 => splice, push