React 스터디 3주차

🎮 웹 게임을 만들며 배우는 React
3주차: 숫자야구

1. import

import { hot } from '/...'

에서 { hot } 은 구조분해 문법이다. default로 export되지 않은 컴포넌트를 import할 때 사용한다.

2. React의 반복문

React에서는 반복문을 이용할 때 map 함수를 사용한다.

          {['사과', '바나나','포도','귤','감'].map((v) => {
              return (
                <li>{v}</li>
              )
          })}

달라지는 부분을 배열로 만들어 이용하면 된다.

이런 방식으로도 반복문 배열 선언이 가능하며

객체 형태로도 사용할 수 있다. 상당히 중요한 함수!


3. 컴포넌트 분리

                <ul>
                    {this.fruit.map((v, i) => {
                        return (
                            <div>
                                <li key={v.fruit + v.taste}>
                                    <b>{v.fruit}</b> - {i}
                                    <div>컨텐츠1</div>
                                    <div>컨텐츠2</div>
                                    <div>컨텐츠3</div>
                                    <div>컨텐츠4</div>
                                </li>
                            </div>
                        )
                    })}
                </ul>

이 코드를

Try.jsx

import React, { Component } from 'react';

class Try extends Component {
    render() {
        return (
            <div>
                <b>{v.fruit}</b> - {i}
                <div>컨텐츠1</div>
                <div>컨텐츠2</div>
                <div>컨텐츠3</div>
                <div>컨텐츠4</div>
            </div>
        );
    }
}

export default Try;

Try.jsx 파일로 컴포넌트를 분리하여 다음과 같이 표현할 수 있다.

                <ul>
                    {this.fruit.map((v, i) => {
                        return (
                            <div>
                                <Try />
                            </div>
                        )
                    })}
                </ul>

이때 Try.jsx 컴포넌트에서는 v와 i가 선언되지 않은 상태이다. 따라서 당연하게도 오류가 발생하는데, 이때

NumberBaseball.jsx

                    {this.fruit.map((v, i) => {
                        return (
                            <div>
                                <Try value={v} index={i} />
                            </div>
                        )
                    })}

Try.jsx

import React, { Component } from 'react';

class Try extends Component {
    render() {
        return (
            <div>
                <b>{this.props.v.fruit}</b> - {this.props.index}
                <div>컨텐츠1</div>
                <div>컨텐츠2</div>
                <div>컨텐츠3</div>
                <div>컨텐츠4</div>
            </div>
        );
    }
}

export default Try;

이와 같이 props를 지정해 주면 된다! 이때, props는 부모 컨테이너로부터 자식 컨포넌트가 받아온 값이다. 따라서 props의 값을 바꿀 경우, 부모 컨테이너로부터 접근해 주어야 한다.

하지만 자식 컨테이너에서 props의 값을 변경해야 하는 상황이 생기면, props를 state에 넣어주어 해결한다.

const Try = memo(({ tryInfo }) => {
    const [result, setResult] = useState(tryInfo.result);
    
    const onClick = () => {
        setResult('1');
    }
    return (
        <li>
            <div>{tryInfo.try}</div>
            <div onClick={onClick}>{result}</div>
        </li>
    );
});

4. 숫자야구

1. 클래스형 컴포넌트

import React, { Component } from 'react';
import Try from './Try';

function getNumbers() {
    // 숫자 네 개를 겹치지 않고 뽑아내는 함수
    const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    const array = [];
    for (let i = 0; i < 4; i+= 1) {
        const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
        array.push(chosen);
    }

    return array;
}

class NumberBaseball extends Component {
    state = {
        result: '',
        value: '',
        answer: getNumbers(),
        tries: [],
    };

    onSubmitForm = (e) => {
        const { value, tries, answer } = this.state;
        e.preventDefault();
        // 화살표 함수를 안 쓰면 this 사용 불가 (undefined)
        if(this.state.value === this.state.answer.join('')) {
            this.setState({
                result: '홈런!',
                tries: [...this.state.tries, { try: value, result: '홈런!'}],
                // 옛날 배열, 새로운 값 <- 의 형태!
            })
        } else {
            const answerArray = value.split('').map((v) => parseInt(v))
            let strike = 0;
            let ball = 0;
            if (this.state.tries.length >= 9) { // 10번 이상 틀렸을 때
                this.setState({
                    result: `10번 이상 실패! 답은 ${this.state.answer.join(',')}이었습니다.`
                });

                alert('신규 게임 시작')
                this.setState({
                    value: '',
                    answer: getNumbers(),
                    tries: [],
                });

            } else {
                for (let i = 0; i <4; i += 1) {
                    if (answerArray[i] === this.state.answer[i]) {
                        strike += 1;
                    }

                    else if (this.state.answer.includes(answerArray[i])) {
                        ball += 1;
                    }
                }
                this.setState({
                    tries: [...this.state.tries, { try: this.state.value, result: `${strike} 스트라이크, ${ball} 볼입니다.`}]
                })
            }
        }
    };

    onChangeInput = (e) => {
        console.log(this.state.answer);

        this.setState({
            value: e.target.value,
        });
    };;

    render() {
        return (
            <div>
                <h1>{this.state.result}</h1>
                <form onSubmit={this.onSubmitForm}>
                    <input maxLength={4} value={this.state.value} 
                        onChange={this.onChangeInput}/>
                </form>

                <div>시도: {this.state.tries.length}</div>

                <ul>
                    {this.state.tries.map((v, i) => {
                        return (
                            <Try key={`${i + 1}차 시도`} tryInfo={v} />
                        )
                    })}
                </ul>
            </div>
        );
    }
}

export default NumberBaseball;

2. 함수형 컴포넌트

import React, { useState } from 'react';
import Try from './Try';

function getNumbers() {
    // 숫자 네 개를 겹치지 않고 뽑아내는 함수
    const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    const array = [];
    for (let i = 0; i < 4; i+= 1) {
        const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
        array.push(chosen);
    }
    return array;
}

const NumberBaseball = () => {
    const [result, setResult] = useState('');
    const [value, setVaule] = useState('');
    const [answer, setAnswer] = useState(getNumbers());
    const [tries, setTries] = useState('');

    const onSubmitForm = (e) => {
        e.preventDefault();
        // 화살표 함수를 안 쓰면 this 사용 불가 (undefined)
        if(value === answer.join('')) {
            setResult('홈런!');
            setTries((prevTries) => {
                return [...prevTries, { try: value, result: '홈런!'}]
            });

            alert('신규 게임 시작');
            setVaule('');
            setAnswer(getNumbers());
            setTries([]);

        } else { // 답 틀렸으면
            const answerArray = value.split('').map((v) => parseInt(v))
            let strike = 0;
            let ball = 0;
            if (tries.length >= 9) { // 10번 이상 틀렸을 때
                
                setResult(`10번 이상 실패! 답은 ${answer.join(',')}이었습니다.`);

                alert('신규 게임 시작')
                setVaule('');
                setAnswer(getNumbers());
                setTries([]);

            } else {
                for (let i = 0; i <4; i += 1) {
                    if (answerArray[i] === answer[i]) {
                        strike += 1;
                    }

                    else if (answer.includes(answerArray[i])) {
                        ball += 1;
                    }
                }

                setTries((prevTries) => [...prevTries, { try: value, result: `${strike} 스트라이크, ${ball} 볼입니다.`}],)
                setVaule('');
            }
        }
    };

    const onChangeInput = (e) => {
        setVaule(e.target.value);
    };;

    return (
        <div>
            <h1>{result}</h1>
            <form onSubmit={onSubmitForm}>
                <input maxLength={4} value={value} 
                    onChange={onChangeInput}/>
            </form>

            <div>시도: {tries.length}</div>

            <ul>
                {tries && tries.map((v, i) => {
                    return (
                        <Try key={`${i + 1}차 시도`} tryInfo={v} />
                    )
                })}
            </ul>
        </div>
    );
}

export default NumberBaseball;

5. shouldComponentUpdate

import React, { Component } from 'react';

class RenderTest extends Component {
    state = { 
        counter: 0,
    };

    onClick = () => {
        this.setState({});
    }

    render() {
        console.log('렌더링', this.state);

        return (
            <div>
                <button onClick={this.onClick}>클릭</button>
            </div>
        )
    }
}

export default RenderTest;

이때 <클릭> 버튼을 누르면, 바뀌는 부분이 없음에도 불구하고 렌더링이 된다!
리액트에서는 setState를 호출하기만 해도 렌더링이 일어나기 때문이다.

이런 상황에 사용하는 것이 바로 shouldUpdateComponent이다.

    shouldComponentUpdate(nextProps, nextState, nextContext) {
        if(this.state.counter !== nextState.counter) {
            return true;
        }
        return false;
    }

shouldUpdateComponent를 사용하면, 바뀌는 게 없을 때에는 렌더링이 일어나지 않는다.
이와 같이 React를 사용할 때에는 최적화를 하는 연습을 많이 하는 것이 좋다.

만약 shouldUpdateComponent를 사용하는 것이 번거롭다면 PureComponent를 사용할 수도 있다. 다만, 컴포넌트가 복잡해지는 경우에는 사용이 어려울 수 있다.


class RenderTest extends PureComponent {
    state = { 
        counter: 0,
    };

    onClick = () => {
        this.setState({});
    }

    render() {
        console.log('렌더링', this.state);

        return (
            <div>
                <button onClick={this.onClick}>클릭</button>
            </div>
        )
    }
}

export default RenderTest;

Hooks에서 동일한 기능을 구현하려면, React.memo를 사용하면 된다.

import React, { memo } from 'react';

const Try = memo(({ tryInfo }) => {
    return (
        <li>
            <div>{tryInfo.try}</div>
            <div>{tryInfo.result}</div>
        </li>
    );
});

export default Try;

추가

1.

Hooks 컴포넌트에 memo를 추가하다가 발생한 오류이다.


오류 메시지가 Missing semicolon이라고 해서 이게 대체 무슨 말인가 했었는데, 소괄호로 한 번 더 감싸주어야 하는 것을 잊어 발생한 오류이다... 😥



이번 주차는 생각보다 난이도가 있어 여러 날에 걸쳐 강의를 수강했다. 꾸준히 복습해야겠다 😲

좋은 웹페이지 즐겨찾기