인프런에 올라간 제로초님의 강의를 보고 정리한 내용입니다.
https://www.inflearn.com/course/web-game-react
import React, {Component, createRef} from 'react';
import Try from './Try.js';
function getNumbers() { //숫자 4개를 랜덤하게
const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const array = [];
for (let i = 0; i < 4; i++) {
const selected = numberList.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
array.push(selected);
}
console.log(array);
return array;
}
class Baseball extends React.Component {
state = {
result : "",
value : "",
answer : getNumbers(),
tries : [],
};
onSubmitForm = (e) => {
e.preventDefault();
console.log(this.state.answer.join('') + ' ' + this.state.value);
if (this.state.answer.join('') === this.state.value)
{
this.setState((prevState) => {
return {
result : "홈런!",
tries : [...this.state.tries, {try: this.state.value, result:"홈런!"}],
value : "",
}
});
alert("홈런!");
alert("게임 재시작");
this.setState({
result : "",
value : "",
answer : getNumbers(),
tries : [],
})
}
else
{
const answerArray = this.state.value.split('').map((e)=>parseInt(e));
let strike = 0;
let ball = 0;
if (this.state.tries.length >= 9) {
this.setState({result: `실패! 정답은 ${answerArray.join('')}입니다.`});
alert("게임 재시작");
this.setState({
result : "",
value : "",
answer : getNumbers(),
tries : [],
})
} else {
for (let i = 0; i < 4; i++)
{
if (answerArray[i] === this.state.answer[i])
strike++;
else if (this.state.answer.includes(answerArray[i]))
ball++;
}
this.setState((prevState) => {
return {
result : `${strike}스크라이크 ${ball}볼`,
tries : [...this.state.tries, {try: this.state.value, result:`${strike}스크라이크 ${ball}볼`}],
value : "",
}
});
}
this.inputRef.current.focus();
}
};
onChangeInput = (e) => {
this.setState({value : e.target.value});
};
inputRef = createRef();
render() {
const {tries, value} = this.state;
return (
<>
<h1>숫자야구</h1>
<form onSubmit={this.onSubmitForm}>
<input ref={this.inputRef} maxLength={4} value={value} onChange={this.onChangeInput}/>
</form>
<div>시도 : {tries.length}</div>
<ul>
{tries.map((v, i) => {return(<Try key={i} tryInfo={v}/>)})}
</ul>
</>
)}
}
export default Baseball;
import React, {PureComponent} from 'react';
class Try extends PureComponent {
render() {
const { tryInfo } = this.props;
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
)
}
}
*/
export default Try;
import React, {useRef, useState, memo} from 'react';
import Try from './Try.js';
function getNumbers() { //숫자 4개를 랜덤하게
const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const array = [];
for (let i = 0; i < 4; i++) {
const selected = numberList.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
array.push(selected);
}
console.log(array);
return array;
}
const Baseball = memo(() => {
const [result, setResult] = useState('');
const [value, setValue] = useState('');
const [answer, setAnswer] = useState(getNumbers());
const [tries, setTries] = useState([]);
const inputEl = useRef(null);
const onSubmitForm = (e) => {
e.preventDefault();
console.log(answer.join('') + ' ' + value);
if (answer.join('') === value)
{
setResult("홈런!");
setTries((prevTries) => { return [...prevTries, {try : value, result : "홈런!"}] });
setValue();
alert("홈런!");
alert("게임 재시작");
setResult("");
setValue("");
setAnswer(getNumbers());
setTries([]);
}
else
{
const answerArray = value.split('').map((e)=>parseInt(e));
let strike = 0;
let ball = 0;
if (tries.length >= 9) {
alert(`실패! 정답은 ${answerArray.join('')}입니다.`);
alert("게임 재시작");
setResult("");
setValue("");
setAnswer(getNumbers());
setTries([]);
}
else {
for (let i = 0; i < 4; i++)
{
if (answerArray[i] === answer[i])
strike++;
else if (answer.includes(answerArray[i]))
ball++;
}
setResult(`${strike}스크라이크 ${ball}볼`);
setTries((prevTries) => {
return [...prevTries, {try : value, result : `${strike}스크라이크 ${ball}볼`}]});
setValue("");
};
};
inputEl.current.focus();
};
const onChangeInput = (e) => {
setValue(e.target.value);
};
return (
<>
<h1>숫자야구</h1>
<form onSubmit={onSubmitForm}>
<input ref={inputEl} maxLength={4} value={value} onChange={onChangeInput}/>
</form>
<div>시도 : {tries.length}</div>
<div>결과 : {result}</div>
<ul>
{tries.map((v, i) => {return(<Try key={i} tryInfo={v}/>)})}
</ul>
</>
)
})
import React, {memo} from 'react';
const Try = memo(({tryInfo}) => {
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
)
});
export default Try;
require
module.exports = {리엑트 컴포넌트 이름}
const {객체} = require('{파일 주소나 모듈 이름}');
import
export default "{리액트 컴포넌트 이름}"
import {객체} from "{파일 주소나 모듈 이름}"
import 할 때 default로 export 된 것이 아닌 것들은 중괄호 안에 감싸서 import해야 한다.
export default는 한 파일에서 한 번씩만 쓸 수 있다. 주로 컴포넌트를 export 할 때 사용된다. 그냥 export하는 것은 얼마든지 가능하다.
export const hello = 'hello'
hello 객체를 'hello'라는 이름으로 내보낸다.
엄밀히 따지면 두 표기법은 다르지만 지금은 호환된다는 선에서 멈춰도 된다.
숫자 야구.
사용자가 만든 메서드를 쓸 때는 항상 화살표 함수를 써야 한다.
value와 onChange, value와 defaultValue는 한 세트.
https://velog.io/@daybreak/Javascript-map함수
<ul>
{['사과', '배', '포도'].map( (e) => {
return <li>{e}</li>
})}
</ul>
배열의 각 원소들을 앞에서부터 하나씩 읽는다. 보통 화살표 함수와 같이 쓰인다.
['a', 'b', 'c', 'd'].map((e)=>{console.log({e})})
일차원 배열이 아닌 key, value형태를 요구할 시, 다른 방법을 사용해야 한다.
<ul>
{[['사과', '달다'], ['배', '시원하다'], ['포도', '시큼하다']].map( (e) => {
return <li>{e[0]} {e[1]}</li>
})}
</ul>
<ul>
{[{name : '사과', taste : '달다'}, {name : '배', taste : '시원하다'}, {name : '포도', taste : '시다'}].map( (e) => {
return <li>{e.name} {e.taste}</li>
})}
</ul>
<ul>
{[{name : '사과', taste : '달다'},
{name : '배', taste : '시원하다'},
{name : '포도', taste : '시다'}].map((e) => <li key={e.name + e.taste}>{e.name} {e.taste}</li>)}
</ul>
화살표 함수에서는 리턴, 소괄호를 생략해줄 수 있다.
파일을 쪼개 여러 개의 컴포넌트를 최종적으로 하나의 파일에서 렌더링하게 할 수 있다.
컴포넌트에 필요한 정보가 다른 파일에 있을 때 해당 컴포넌트를 사용할 때 같이 보내주어야 한다.
Redux vs MobX vs Context API
{/* */}
{메서드}(e){
console.log(this);
}
//undefined
{메서드} = (e) => {
console.log(this);
}
//클래스 객체
화살표 함수를 사용해야만 this가 해당 클래스 메서드를 가리키게 된다.
constructor(props) {
super(props);
this.state = {
result : '',
value : '',
answer : getNumbers(),
tries : [],
};
this.onSubmitForm = this.onSubmitForm.bind(this);
this.onChangeInput = this.onChangeInput.bind(this);}
함수처럼 쓰고 싶다면 constructor를 사용해야 한다.
화살표 함수가 bind(this) 역할을 자동으로 해주는 것.
리액트에서 배열에 값을 추가할 때 push()를 쓰면 안된다.
const arr = [];
arr.push(1);
console.log(arr === arr); //ture
const new_arr = [...arr, 2];
console.log(arr === new_arr) //false
기존 배열을 복사해서 만든 새로운 배열을 할당해주는 방식으로 배열을 갱신해야 한다.
import React from 'react';
import Try from './Try.js';
function getNumbers() { //숫자 4개를 랜덤하게
const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const array = [];
for (let i = 0; i < 4; i++) {
const selected = numberList.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
array.push(selected);
}
console.log(array);
return array;
}
class Baseball extends React.Component {
state = {
result : "",
value : "",
answer : getNumbers(),
tries : [],
};
onSubmitForm = (e) => {
e.preventDefault();
console.log(this.state.answer.join('') + ' ' + this.state.value);
if (this.state.answer.join('') === this.state.value)
{
this.setState({
result : "홈런!",
tries : [...this.state.tries, {try: this.state.value, result:"홈런!"}],
value : "",
})
alert("홈런!");
alert("게임 재시작");
this.setState({
result : "",
value : "",
answer : getNumbers(),
tries : [],
})
}
else
{
const answerArray = this.state.value.split('').map((e)=>parseInt(e));
let strike = 0;
let ball = 0;
if (this.state.tries.length >= 9) {
this.setState({result: `실패! 정답은 ${answerArray.join('')}입니다.`});
alert("게임 재시작");
this.setState({
result : "",
value : "",
answer : getNumbers(),
tries : [],
})
} else {
for (let i = 0; i < 4; i++)
{
if (answerArray[i] === this.state.answer[i])
strike++;
else if (this.state.answer.includes(answerArray[i]))
ball++;
}
this.setState({
result : `${strike}스크라이크 ${ball}볼`,
tries : [...this.state.tries, {try: this.state.value, result:`${strike}스크라이크 ${ball}볼`}],
value : "",
})
}
}
};
onChangeInput = (e) => {
this.setState({value : e.target.value});
};
render() {
return (
<>
<h1>숫자야구</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} tryInfo={v}/>)})}
</ul>
</>
)}
}
export default Baseball;
{배열}.splice()
Math.floor()
Math.random()
{배열}.push()
{배열}.join('')
{배열}.includes()
리액트에서의 반복문은 대부분 map을 사용한다.
render() {
const {}
return (
<>
<h1>숫자야구</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} tryInfo={v}/>)})}
</ul>
</>
)}
}
state의 속성을 사용할 때 구조 분해로 state의 속성을 변수에 대입하면 this.state를 생략할 수 있다.
메서드가 아닌 함수(클래스 밖에 배치)로 할 수 있는 경우this를 사용하지 않을 경우 클래스 바깥에 뺄 수 있다.
class exampleClass extends React.Component {
State = {
a : "unchange",
b : 1,
}
this.setState({
a : "change",
b : 3
})
}
const exampleFunction = () => {
[a, setA] = useState("unchange");
[b, setB] = useState(1);
setA("change");
setB(3);
}
클래스 컴포넌트에서 함수 컴포넌트로 변경 시
State → useState import해서 사용.
this.state 삭제
ref 변경
const obj = {a : "123", b : "456", c : "789"};
const func = ({a, b, c}) =>
{
console.log(a, b, c);
}
func(obj);
함수에서 props를 받아올 때 {}안에 props의 속성을 집어넣어서 할당시킬 수 있다.
setState((prevTries) => {
return [...prevTries, {try : value, result : "홈런!"}]
})
옛날 데이터를 새로운 데이터를 만들 때 그대로 쓰게 된다면 화살표 함수를 써주어야만 한다.
//함수 컴포넌트로 변경.
import React, {useState} from 'react';
import Try from './Try.js';
function getNumbers() { //숫자 4개를 랜덤하게
const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const array = [];
for (let i = 0; i < 4; i++) {
const selected = numberList.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
array.push(selected);
}
console.log(array);
return array;
}
const Baseball = () => {
const [result, setResult] = useState('');
const [value, setValue] = useState('');
const [answer, setAnswer] = useState(getNumbers());
const [tries, setTries] = useState([]);
const onSubmitForm = (e) => {
e.preventDefault();
console.log(answer.join('') + ' ' + value);
if (answer.join('') === value)
{
setResult("홈런!");
setTries((prevTries) => {
return [...prevTries, {try : value, result : "홈런!"}]
});
setValue();
alert("홈런!");
alert("게임 재시작");
setResult("");
setValue("");
setAnswer(getNumbers());
setTries([]);
}
else
{
const answerArray = value.split('').map((e)=>parseInt(e));
let strike = 0;
let ball = 0;
if (tries.length >= 9) {
setResult(`실패! 정답은 ${answerArray.join('')}입니다.`);
alert("게임 재시작");
setResult("");
setValue("");
setAnswer(getNumbers());
setTries([]);
} else {
for (let i = 0; i < 4; i++)
{
if (answerArray[i] === answer[i])
strike++;
else if (answer.includes(answerArray[i]))
ball++;
}
setResult(`${strike}스크라이크 ${ball}볼`);
setTries((prevTries) => {
return [...prevTries, {try : value, result : `${strike}스크라이크 ${ball}볼`}]
});
setValue("");
}
}
};
const onChangeInput = (e) => {
(e.target.value);
};
return (
<>
<h1>숫자야구</h1>
<form onSubmit={onSubmitForm}>
<input maxLength={4} value={value} onChange={onChangeInput}/>
</form>
<div>시도 : {tries.length}</div>
<ul>
{tries.map((v, i) => {return(<Try key={i} tryInfo={v}/>)})}
</ul>
</>
)
}
export default Baseball;
함수 컴포넌트로 변경.
props를 활용하다 보면 문제가 많이 생긴다.
크롬 확장 프로그램 중 React DevTool을 사용하면 리액트 컴포넌트를 확인할 수가 있다.
배포 모드에서는 소스 코드, 압축 및 최적화가 되어 있다.
기존 코드는 하나의 state를 수정해도 모든 컴포넌트가 랜더링하도록 되어 있다.
import React, {Component} from 'react';
class Test extends Component {
state = {
counters: 0,
}
onClick = () => {
this.setState({
counters: 0,
})
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
if (this.state.counter === nextState.counter) {
return false;
}
else {
return true;
}
}
render() {
console.log("렌더링");
return (
<div>
<button onClick={this.onClick}>클릭!</button>
<div>{this.state.counters}</div>
</div>
)
}
}
export default Test;
리액트에서는 setState를 호출하기만 하면 render()를 다시 실행해서 렌더링하게 된다.
이를 막기 위해 shouldComponentUpdate()를 사용해서 함수의 인자인 미래의 값들과 현재의 값들을 비교해 값이 동일하다면 다시 렌더링 되지 않게 해줄 수 있다.
다시 렌더링 되는 것을 막는 또다른 방법은 PureComponent를 만드는 것.
import React, {PureComponent} from 'react';
class Test extends PureComponent {
state = {
counters: 0,
}
onClick = () => {
this.setState({
counters: 0,
})
}
render() {
console.log("렌더링");
return (
<div>
<button onClick={this.onClick}>클릭!</button>
<div>{this.state.counters}</div>
</div>
)
}
}
export default Test;
shouldComponentUpdate()가 자동으로 구현되어 있다.
객체나 배열 같은 복잡한(참조) 구조가 있다면 제대로 작동하지 않을 수 있다.
onClick = () => {
const arr = this.state.array;
arr.push(1);
this.setState({
counters: 0,
array : arr,
})
}
배열을 추가할 때 push()로 추가한다면 참조하는 주소가 동일하기 때문에 변했다고 판단하지 않는다.
onClick = () => {
this.setState ({
counters: 0,
array : [this.state.array, 1],
});
}
그렇기에 더더욱 기존 객체를 그대로 가져오지 말고 기존의 배열에 새로운 값을 추가한 배열을 할당하는 방식을 써주어야 한다.
가급적 클래스 컴포넌트의 state에 객체(object) 구조를 안 쓰는 것이 좋다.
//Try.js
import React, {PureComponent} from 'react';
//클래스 컴포넌트
class Try extends PureComponent {
render() {
const { tryInfo } = this.props;
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
)
}
}
export default Try;
기존의 코드 수정.
컴포넌트가 복잡해지면 퓨어컴포넌트가 안 먹힐 수도 있고, 컴포넌트를 써야 하는 경우도 많다.
컴포넌트에 shouldComponentUpdate()를 쓰는 경우도 많다.
import React, {PureComponent, memo} from 'react';
//함수 컴포넌트
const Try = memo(({tryInfo}) => {
return (
<li>
<div>{tryInfo.try}</div>
<div>{tryInfo.result}</div>
</li>
)
});
export default Try;
함수 컴포넌트의 경우 컴포넌트를 memo()함수로 감싸주면 된다.
자식 컴포넌트가 모두 퓨어 컴포넌트나 memo가 적용되었다면 부모도 똑같이 적용시켜도 무방하다.
DOM에 접근하기 위해 해당 태그 속성에 ref를 넣을 때 클래스와 함수 컴포넌트 각각 방법이 다름.
원하는 태그에 ref={(c) => this.input = c;}
, 혹은 ref={inputRef}
(메서드에서 (c) => this.input = c;
구문 실행).
클래스 내부의 input
이라는 객체로 해당 태그에 접근 가능.
this.input.focus();
useRef를 import해서 사용.
const input = useRef(null);
원하는 태그에 ref={input}
로 설정.
input로 해당 태그 접근 가능.
input.current.focus();
클래스 컴포넌트에서도 함수 컴포넌트처럼 ref를 사용할 수 있게 해준다.
createRef를 import해서 사용.
input = createRef();
원하는 태그에 ref={input}
으로 메서드 호출.
input으로 접근가능.
함수처럼 this.input.current.focus();
createRef가 만능이냐면 그것은 아니다. 편리하기는 하지만 클래스 컴포넌트에서 ref로 함수를 호출하게 되면 조금 더 디테일한 설정이 가능해진다.
방식의 차이에 대해 이해하면 내가 원하는 것을 정확하게 구현할 수 있다.
render()안에서 this.setState()를 호출하면 setState → state값 변경 → render() 호출 무한반복이 일어난다.
부모 컴포넌트가 전달해준 props을 자식 컴포넌트가 바꾸려고 해서는 안된다. 자식에 의해 부모의 state가 바뀌게 된다.
정밀한 작업이 필요할 때 constructor를 사용하는 방법이 있다.
클래스에서 constructor(props)를 사용시 항상 super(props)를 써줘야 한다.
A → B → C → D 형태로 컴포넌트 간 자식 관계가 형성되어 있다고 가정하자.
만일 A 에서 D로 props를 전달하고 싶을 때, 위의 경로대로 전달하게 되면 쓸데없이 렌더링 되는 경우가 생길 수 있다.
A에서 D로 직통으로 전달할 수 있는 방법이 바로 컨텍스트다.
진화관계 ) props < 컨텍스트 < 리덕스