Hooks 변경한 코드

import React, {Component, useState, useRef} from "react";
import TryHooks from "./TryHooks";

function getNumbers(){
    const numbers = new Array(9).fill(0).map((v,i)=>v+i+1);
    const shuffle = [];
    while(numbers.length > 0){
        const rand = Math.floor(Math.random() * numbers.length);
        const number = numbers.splice(rand,1)[0];
        shuffle.push(number);
    }
    console.log(shuffle);
    return shuffle.slice(0,4);
}

const NumberBaseballHooks = () => {
    const [value, setValue] = useState('');
    const [result, setResult] = useState('');
    const [answer, setAnswer] = useState(getNumbers());
    const [tries, setTries] = useState([]);
    const inputRef = useRef(null);
    const onSubmitForm = e => {
        e.preventDefault();
        const answerStr = answer.join('');
        if(answerStr === value){
            //홈런
            setResult('홈런!');
            setValue('');
            setTries((prev)=>{
                return [...prev, `${value}:홈런!`]
            });
            setTimeout(()=>{
                alert('게임을 다시 시작합니다.');
                setResult('');
                setAnswer(getNumbers());
                setTries([]);
            },100);

        } else {
            if(tries.length >= 9){
                setResult(`10번 넘게 틀려서 실패! 정답은 ${answerStr} 였습니다`);
                setValue('');
                alert('게임을 다시 시작합니다.');
                setResult('');
                setAnswer(getNumbers());
                setTries([]);
            }else{
                //볼, 스트라이크 판단
                let ball = 0;
                let strike = 0;
                for(let i=0; i<4; i++){
                    if(answerStr[i] === value[i]){
                        strike++;
                    }else if(answerStr.includes(value[i])){
                        ball++;
                    }
                }
                setTries((prev)=>{
                    return [...prev, `${value}:${strike}S ${ball}B`];
                })
                setResult('땡');
                setValue('');
            }
        }
        inputRef.current.focus();
    }

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

    return (
        <>
            <h1>{result}</h1>
            <form onSubmit={onSubmitForm}>
                <input type="text" maxLength={4} ref={inputRef} value={value} onChange={onChangeInput}/>
                <button>입력</button>
            </form>
            <div>시도: {tries.length}</div>
            <ul>
                {
                    tries.map((v,i) => {
                        return (<TryHooks key={v+i} value={v} index={i+1} />);
                    })
                }
            </ul>
        </>
    )
}
export default NumberBaseballHooks;

NumberBaseballHooks.jsx

 

import React from "react";

const TryHooks = ({index, value}) => {
    return (
        <li>
            {index}. <b>{value}</b>
        </li>
    )
}
export default TryHooks;

TryHooks.jsx

 

 

근데 여기서 문제가 있었다.

input에 value를 변경하면 setValue가 value state를 변경하면서 리렌더가 되는데 리렌더가 되면서 NumberBaseballHooks 내부의 로직이 재실행 되면서 getNumbers()까지 재호출 되는 문제가 있었다.

그래서 위의 사진과 같이 console.log가 input에 타자 칠 때마다 나오는 것.

 

const [answer, setAnswer] = useState(getNumbers());

해당 코드가 문제였는데 저렇게 한다고해서 state가 리렌더 할 때마다 바뀌는 것은 아니고 호출만 여러 번 되는것이다.

하지만 호출이 되는 것도 문제이다. 

useState가 자동으로 getNumbers()를 첫 번째 호출한 리턴값만 state에 저장하고 그 다음부터는 리턴값을 무시해버린다. 하지만 리렌더 시에 getNumbers함수가 계속 재호출 되긴한다. (호출값을 넣어버렸기 때문에) 만약 getNumbers가 아주 오래걸리는 작업을 하는 함수라면 매우 비효율적이다.

 

그럴 때는 아래와 같이 호출해서 넘기지 말고 함수 자체를 넘기면 된다. (lazy init 이라고 한다.)

const [answer, setAnswer] = useState(getNumbers);

 

useState 용법이 2가지고 있는데

1. useState(초기값) - 일반적으로 쓰이는 경우로 초기값으로 state를 설정해준다.

2. useState(함수) - 함수를 그대로 넣는 경우인데 함수의 리턴값이 초기값으로 설정이 되지만 그 다음부터 이 함수는 실행되지 않는다. 

1. 구조분해 할당으로 state, props 간편하게 사용하기

onSubmitForm = e => {
	const {result, value, tries,answer} = this.state;
 	...
}

render(){
	const {result, value, tries} = this.state;
    ...
}

NumberBaseball.js

const {index, value} = this.props;

Try.js

2. class 밖으로 뺄 수 있는 함수

this를 사용하지 않으면 class 밖으로 뺄 수 있다. (getNumbers 처럼)

 

리액트에서 state를 직접 바꾸는건 금지!

리랜더해주는 조건이 예전 state와 현재 state가 달라야 리랜더를 해주는데 직접 바꿔버리면 달라지지 않는다.

그래서 불변성을 잘 지켜야함. 

특히 객체나 배열! 주의

tries: [...tries, `${this.state.value}: 홈런!`],

-기존 배열을 유지시켜주고 싶다면 spred 연산자를 사용해주면 좋다.

-this.state.tries[0] = 10;, this.state.tires.push(20);      <----이런식으로 하면 안됌!!

 

직접 만든 전체코드

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

//숫자 내게 랜덤으로 뽑기 함수
function getNumbers(){
    const numbers = new Array(9).fill(0).map((v,i)=>v+i+1);
    const shuffle = [];
    while(numbers.length > 0){
        const rand = Math.floor(Math.random() * numbers.length);
        const number = numbers.splice(rand,1)[0];
        shuffle.push(number);
    }
    console.log(shuffle);
    return shuffle.slice(0,4);
}


class NumberBaseball extends Component{
    state = {
        value:'',
        result:'',
        answer : getNumbers(),
        tries:[],
    }
    onChangeInput = e => {
        const value = e.target.value;
        this.setState({value});
    }

    onSubmitForm = e => {
        e.preventDefault();
        const tries = this.state.tries;
        const ans = this.state.answer.join('')
        if(this.state.value === ans){
            this.setState({
                result: '홈런!',
                value: '',
                tries: [...tries, `${this.state.value}: 홈런!`],
            });
            alert('게임을 재시작합니다.');
            this.setState({
                value:'',
                answer: getNumbers(),
                tries: [],
                result:'',
            })
        } else {
            //볼, 스트라이크 판단
            let ball = 0;
            let strike = 0;

            if(this.state.tries.length >= 9){
                this.setState({
                    result : `10번 넘게 틀려서 실패! 정답은 ${ans} 였습니다`,
                    tries,
                });
                setTimeout(()=>{
                    alert('게임을 재시작합니다.');
                    this.setState({
                        value:'',
                        answer: getNumbers(),
                        tries: [],
                        result:'',
                    })
                },10);
            }else{
                for (let i = 0; i < this.state.value.length; i++) {
                    if (this.state.value[i] === ans[i]) {
                        strike++;
                    } else if (this.state.value.indexOf(ans[i]) > -1) {
                        ball++;
                    }
                }
                if(ball === 0 && strike === 0){ //out
                    tries.push(`${this.state.value}: OUT!`);
                }else{
                    tries.push(`${this.state.value}: ${strike}S ${ball}B`);
                }

                this.setState({
                    value: '',
                    result:'땡',
                    tries,
                });
            }

        }
        this.input.focus();
    }

    input;
    onRefInput = c =>{
        this.input = c;
    }
    fruits = [
        {fruit: '사과', taste:'맛없다.'},
        {fruit: '바나나', taste:'맛없다.'},
        {fruit: '호랑이', taste:'맛없다.'},
        {fruit: '구울', taste:'맛없다.'},
        {fruit: '구렁이', taste:'맛없다.'},
        {fruit: '고우와니', taste:'맛없다.'},
    ]

    render() {
        return (
            <>
                <h1>{this.state.result}</h1>
                <form onSubmit={this.onSubmitForm}>
                    <input type="text" maxLength={4} ref={this.onRefInput} value={this.state.value} onChange={this.onChangeInput}/>
                    <button>입력</button>
                </form>
                <div>시도: {this.state.tries.length}</div>
                <ul>
                    {
                        this.state.tries.map((v,i) => {
                            return (<Try key={v+i} value={v} index={i+1} />);
                        })
                    }
                </ul>
            </>
        );
    }
}
export default NumberBaseball;

NumberBaseball.jsx

 

import React, {Component} from "react";

class Try extends Component{
    render() {
        return (
        <li>
            {this.props.index}. <b>{this.props.value}</b>
        </li>
        );
    }
}
export default Try;

 

Try.jsx

리액트의 대부분의 문제는 props에서 발생한다.

자식의 자식에게 물려준다거나 더 깊이 들어가는 복잡한 경우가 생긴다.

그것을 방지하기 위해서 컨텍스트, 리덕스 같은 기술이 쓰인다.

(리액트에는 컨텍스트가 있고 컨텍스트가 좀 더 복잡한 일을 할 수 있게 만든 것이 리덕스(은행역할))

 

리액트 JSX 에서 주석처리하기

{/*<h1>{this.state.result}</h1>*/}

 

바인딩 부분은 영상을 보는게 나을 것 같다.

중요한 내용은 아니고 class 컴포넌트에서 화살표 함수를 쓰는 이유를 알려주심. (this를 신경쓰지 않기 위해서.. 화살표함수는 bind(this);를 자동으로해준다.)

 

컴포넌트로 나누는 이유

-성능 최적화에 더 좋다.(성능 최적화 시간에 자세히 다룬다고함)

-가독성이 좋아진다. (메인 파일의 코드가 길어지는 것을 방지할 수 있다.)

-코드 관리가 쉬워진다.

-재사용성이 좋아진다.

 

Props

-부모 컴포넌트에서 자식 컴포넌트에 데이터를 전달하는 방법임.

this.props.value

class component에서 props 받는방법.

map에 관해서는 적지 않는다.

 

map을 돌릴 때는 항상 key라는 것을 추가해줘야한다.

key는 react가 성능 최적화 할 때 사용하는 값이고, 고유한 값을 적어줘야한다.

key에 index를 넣어주는 경우가 있는데 이런 경우는 성능 최적화 할 때 문제가 생기기 때문에 index를 넣으면 안된다.

 

 

require

require : node의 모듈 시스템

내보내기 module.exports 이름; (= export default) 엄밀히 따지면 다른데 react 하는 데에는 호환된다고함.

가져오기 const 변수명 require('이름');

 

노드 모듈 시스템에서 

module.exports = { hello: 'a' };

exports.hello = 'a'; 는 같다.

 

import

ES2015문법으로 Node의 모듈과 다르다.

내보내기 export default 이름; (한번만 가능)   import { hello };

               export 이름; (여러번 가능)               import Numberbaseball;

가져오기 import 이름 from '이름';

 

사실 import쓰면 에러가 나는데 babel이 import들을 모두 require로 변경해준다고함.

Node에서는 require쓰고, react에서는 import와 exports 쓴다.

 

webpack

웹팩은 노드가 돌려주는 것이기 때문에 require를 써야한다!!

client부분(jsx)는 babel이 import를 require로 변경해주기 때문에 import를 사용해도 된다.

 

 

공통점은 exports 되는게 객체나 배열이면 구조분해 가능!

ex) import {useState, useRef} from 'react';

컨트롤드 인풋 vs 언컨트롤드 인풋

컨트롤드 인풋 : state로 만들어진 value, 해당 value를 변경할 수 있는 onChange(setState)

언컨트롤드 인풋 : value, onChange가 없는 것 (원시적인 인풋 형태)

  --> <input type='text', ref={inputRef} defaultValue="하이" />

(언컨트롤드 인풋에 value를 넣는건 컨트롤드 인풋으로 간주될 수 있기 때문에 언컨트롤드 인풋에서 기본값을 넣으려면 defalutValue를 사용해야한다.)

쉽게 말해서 input의 value가 onSubmit에서만 동작하는 경우 unControlledInput을 사용해도 된다.

 

컨트롤드 인풋을 사용할 때는?

예를 들면 아래와 같은 경우임.

 

1. 비밀번호 체크할 때 valid해서 밑에 빨간줄 뜨게 해야한다. (dynamic inputs)

2. 비밀번호 체크할 때 조건에 안맞으면 submit 불가능하도록 해야한다. (conditionally disabling submit button)

3. 비밀번호 검증하는 것

4. 비밀번호 형식 강제하는 것

뭐 등등 많은데 사실 몰라도 되고 그냥 컨트롤드 인풋만 사용하면 된다고 하심.

//필요로 하는 패키지나 라이브러리 불러오기
const React = require('react');
const {useState, useRef} = React;


const WordRelayHooks = () => {
    const [word, setWord] = useState('제로초');
    const [value, setValue] = useState('');
    const [result, setResult] = useState('');
    const inputRef = useRef(null);

    const onChange = e => {
        const value = e.target.value;
        setValue((prev)=>value);
    }
    const onSubmit = e => {
        e.preventDefault();
        const lastChar = word[word.length-1];
        const input = value;
        if(lastChar === input[0]){
            setResult('딩동댕');
            setWord((prev)=>input);
            setValue((prev)=>'');
        }else{
            setResult('땡');
            setValue('');
        }
        inputRef.current.focus();
    }

    return (
        <>
            <div>{word}</div>
            <form onSubmit={onSubmit}>
                <input ref={inputRef} type='text' onChange={onChange} value={value}/>
                <button>입력</button>
            </form>
            <div>{result}</div>
        </>
    );

}

//쪼갠 파일에서 쓰는 컴포넌트를 밖에서도 사용할 수 있게 해주는 것. (이것이 노드의 모듈 시스템)
module.exports = WordRelayHooks;

WordRelayHooks.jsx

 

const React = require('react');
const ReactDom = require('react-dom');
const WordRelay = require('./WordRelay'); //module.export를 했기 때문에 require로 가져올 수 있다.
const WordRelayHooks = require('./WordRelayHooks');

//ReactDom.render(<WordRelay/>, document.querySelector('#root'));
ReactDom.render(<WordRelayHooks/>, document.querySelector('#root'));

Client.jsx

코드를 바꿀 때마다 webpack 명령어로 리빌드 하는 것이 매우 귀찮은 일이고, 실수를 유발할 포인트가 매우 크다.

그래서 자동으로 리빌드 되는 방법을 알려주는 강의임.

 

1. 패키지 설치

npm i -D react-refresh 

npm i -D @pmmmwh/react-refresh-webpack-plugin

npm i -D webpack-dev-server //개발용 서버

 

2. package.json 변경

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "dev": "webpack serve --env development"
},

package.json에서 먼저 설치한 패키지들이 잘 설치되었는지 devDependencies를 확인 후 script의 dev 부분을 저렇게 바꿔주자.

webpack serve --env development

 

3. webpack-config.js 변경

3-1. 패키지 require

받았던 패키지 중에 아래 패키지를 require 해준다.

const RefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

 

3-2. webpack plugins 추가

가져온 패키지를 plugins 부분에 넣어주면 장착완료 (plugins에 넣어준 패키지들은 빌드 될 때마다 해당 부분이 실행된다고 생각하면 된다.)

plugins: [//webpack에 대한 plugins (바벨의 plugins와 다르다!)
    new webpack.LoaderOptionsPlugin({debug: true}), //Loader = module,rules 의 options에 debug: true를 넣어주는 것이다.
    new RefreshWebpackPlugin()
],

 

3-3. babel loader plugins 추가

또 바벨로더에도 플러그인을 넣어줘야한다.

('react-refresh/babel', //바벨이 최신 문법을 예전 문법으로 트랜스파일할 떄 핫리로드 기능도 추가해줌.)

//webpack과 연결할 모듈들을 적시한다.
module: {
    rules:[{
        test: /\.jsx?/,
        loader: 'babel-loader',
        options:{ //바벨로더의 옵션
            presets:[['@babel/preset-env',{  //예전 브라우저 지원 문법으로 변경해주는 프리셋
                targets:{   //@babel/preset-env 프리셋에 대한 옵션
                    browsers: ['> 5% in KR','last 2 chrome versions'] // 한국에서 5% 이상, 크롬 최신 전 버전까지만 지원 (browserslist 사이트 참고)
                },
                debug: true
            }], '@babel/preset-react'],
            plugins: [
                //'@babel/plugin-proposal-class-properties',
                'react-refresh/babel', //바벨이 최신 문법을 예전 문법으로 트랜스파일할 떄 핫리로드 기능도 추가해줌.
            ]
        }
    }],
},

이렇게 하면 바벨이 최신 문법을 예전 문법으로 트랜스파일할 때 핫리로드 기능도 추가를 해준다.

 

3-4. 데브 서버 설정 추가

데브서버의 역할 :

1. webpack.config.js에 적어둔 대로 빌드 결과물을 설정해둔 폴더로 저장해둔다. (실제로 파일이 저장되는건 아니고 ram에 저장됨.)

2. index.html을 실행하면 저장했던 결과물을 제공해준다. 

3. hot-reload 기능도 추가했기 때문에 소스에 변경점이 생기면 저장했던 결과물도 자동으로 수정해준다.

devServer: {
    devMiddleware: { publicPath: '/dist' }, //웹팩이 빌드할때 파일을 생성해주는 경로 (RAM에 추가됨)
    static: { directory: path.resolve(__dirname) }, // static은 정적파일의 경로 (index.html 같은거)
    hot: true,
}

 

※ reload vs hot-reload 

리로드는 그냥 새로고침 (기존 데이터가 날아감)

핫리로드는 기존 데이터를 보존하고 화면만 바꿔준다.

데브서버를 추가하면 그냥 리로드는 되는데 핫 리로드는 

npm i -D react-refresh 

npm i -D @pmmmwh/react-refresh-webpack-plugin

해당 두개의 패키지를 설치해야만 한다.

 

지금까지의 webpack.config.js 전체코드

const path = require('path'); //node에 기본적으로 제공해주는 path를 쉽게 조작하기 위한 라이브러리
const webpack = require('webpack');
const RefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
    name: 'word_relay-setting', //어떤 것을 위한 웹팩 설정인지
    mode: 'development', //실서비스에서는 production
    devtool: 'eval', //빠르게
    resolve: {
        extensions: ['.js', '.jsx'] //확장자 적어놓으면 xxx.js, xxx.jsx 알아서 찾아서 빌드한다.
    },
    //여기서 부터가 제일 중요
    //entry : 입력,
    //output: 출력
    entry: {
        app:['./client'] // './WordRelay.jsx' 는 이미 client.jsx에서 불러오고 있기 때문에 굳이 써주지 않아도 된다.
    },

    //webpack과 연결할 모듈들을 적시한다.
    module: {
        rules:[{
            test: /\.jsx?/,
            loader: 'babel-loader',
            options:{ //바벨로더의 옵션
                presets:[['@babel/preset-env',{  //예전 브라우저 지원 문법으로 변경해주는 프리셋
                    targets:{   //@babel/preset-env 프리셋에 대한 옵션
                        browsers: ['> 5% in KR','last 2 chrome versions'] // 한국에서 5% 이상, 크롬 최신 전 버전까지만 지원 (browserslist 사이트 참고)
                    },
                    debug: true
                }], '@babel/preset-react'],
                plugins: [
                    //'@babel/plugin-proposal-class-properties',
                    'react-refresh/babel', //바벨이 최신 문법을 예전 문법으로 트랜스파일할 떄 핫리로드 기능도 추가해줌.
                ]
            }
        }],
    },
    plugins: [//webpack에 대한 plugins (바벨의 plugins와 다르다!)
        new webpack.LoaderOptionsPlugin({debug: true}), //Loader = module,rules 의 options에 debug: true를 넣어주는 것이다.
        new RefreshWebpackPlugin()
    ],
    output: {
        //path.join : 파라미터로 전달된 경로를 합쳐준다.
        //__dirname -> 현재 경로(lecture)
        path: path.join(__dirname, 'dist'), //C:\Users\iLovePC\Desktop\codingapple\zerocho-react-webgame\lecture 에 dist를 더해준다.
        filename: "app.js",
        publicPath: "/dist/",
    },
    devServer: {
        devMiddleware: { publicPath: '/dist' }, //웹팩이 빌드할때 파일을 생성해주는 경로 (RAM에 추가됨)
        static: { directory: path.resolve(__dirname) }, // static은 정적파일의 경로 (index.html 같은거)
        hot: true,
    }
}

+ Recent posts