티스토리 뷰

목차

더보기

강의목차

5-1. 리액트 라이프사이클 소개
5-2. setInterval과 라이프사이클 연동하기
5-3. 가위바위보 게임 만들기
5-4. 고차 함수와 Q&a
5-5. Hooks와 useEffect
5-6. 클래스와 Hooks 라이프사이클 비교

 

 

5-1. 리액트 라이프사이클 소개

//RSPClass.jsx
import React, { Component } from 'react'

//클래스의 경우 - 컴포넌트 Life Cycle
//1. constructor
//2. 처음 render() 실행
//3. render속 ref 설정
//4. compoenentDidMount() 한번 실행
//5. setState/props 바뀔때 -> shouldComponentUpdate(true) -> render() -> componentDidUpdate()
//6. 부모가 나를 없앴을 때 -> componentWillUnmount() -> 소멸

class RSP extends Component {
  // 1
  state = {
    result: '',
    imgCoord: 0,
    score: 0,
  }
  componentDidMount() {} // 3. 컴포넌트가 첫 랜더링된 후 한번만

  shouldComponentUpdate() {} // 5. return true 일 경우

  componentDidUpdate() {} // 6. 리랜더링 후

  componentWillUnmount() {} // 7. 컴포넌트가 제거되기 직전
  // 2, 4(setState/props바뀔때)
  render() {
    return (
      <>
        <div>hello</div>
      </>
    )
  }
}
export default RSP

 

5-2. setInterval과 라이프사이클 연동하기

깃 링크: https://github.com/Harimad/zeroReact/commit/feae0e1ff1706c00158a426831842ead779a88c6

Comment 참고

 


비동기 함수 요청은 componentDidMount() 안에서 많이 쓴다.

비동기 요청 청리는 componentWillUnmount() 안에서 많이 쓴다.

 

 

Q. 클로저 문제 🎁

비동기함수에서 바깥쪽 변수 찾는데 내부함수에서 외부함수 변수 참조하는데  딱히 문제 될거 없지 않나요??

이부분 조금 이해한되는데 조금만 더 설명 부탁 드립니다.

componentDidMount(){
   const {imgCoord} = this.state  // rspCoords.바위('0')
   setInterval(() => {
     //imgCoord 변수 사용
   })
}
//예상했던 실행
//바위 -> 가위 -> 보 -> 바위

//실제 실행
//바위 -> 가위 -> 바위 -> 가위 -> 반복 ...
//바위를 setInterval 비동기 함수 위의 고정된 imgCoord 값 다시 참조 했기 때문

-> 비동기 함수의 경우 자주 발생하는 문제인데요.

imgCoord를 setInterval 바깥에 빼면 imgCoords는 setInterval하기 전 시점으로 고정됩니다.

그래서 setInterval 후에 가위->바위로 바뀌어도 가위로 계속 남아있고, 그걸 참조하게 됩니다.

 

imgCoord가 setInterval 안에 들어있으면 setInterval하는 순간의 imgCoords를 가져옵니다.

따라서 항상 최신 imgCoords값을 가져올 수 있게 됩니다.

 

Q. 비동기 함수 관련 질문 🎁

setInterval과 외부 변수와의 관계를 정확히 이해하지 못했습니다.

참조하는 변수가 객체나 배열일 때만 이 문제가 발생하는건지요?

let a = 0;

this.interval = setInterval(() => {
  console.log(a);
  if (a === 0 ){
    a = -1;
  } else if( a=== -1){
    a = 1;
  } else if( a === 1){
    a = 0;
  }
}, 1000)

이 부분의 경우는 이상없이 잘 동작하는 걸 확인했습니다.

그런데 객체나 배열의 경우에만 비동기 참조관계가 어그러지는건지와

말씀하신 것 처럼 왜 imgCoords 변수가 고정되는지가 궁금합니다.

 

-> 클로저 문제는 반복문과 비동기 함수가 있어야 발생합니다.

for (var i = 0; i < 10; i++) {
  setTimeout(() => console.log(i), 1000);
}

for (let i = 0; i < 10; i++) {
  setTimeout(() => console.log(i), 1000);
}

-> 두 개를 비교해보세요.

 

그렇다는 것은 setInterval이 반복문의 역할을 한다는 말씀이신가요?

그렇게 된다면 setState가 비동기의 역할을 하므로 둘이 만나면 클로저가 발생이 된다는 것으로 이해하면 될까요?

 

-> 클로저 문제는 반복문과 비동기 함수가 만나서 발생하는 현상을 '클로저를 사용해서 해결하는 문제'입니다.

클로저가 원인인 문제가 아닙니다. setInterval이 반복문의 역할을 하는 게 아닙니다.

 

-> 클로저는 함수와 외부 변수와의 관계입니다.

setTimeout 안에 있는 함수랑 바깥의 this.state.imgCoords가 클로저를 생성해서 값이 고정되어버립니다.

imgCoords는 구조분해 할당으로 인해서 참조가 아닌 값이 되는 것도 맞고요.

 

-> 클로저는 함수와 외부 변수와의 관계입니다.

setTimeout 안에 있는 함수랑 바깥의 this.state.imgCoords가 클로저를 생성해서 값이 고정되어버립니다.

imgCoords는 구조분해 할당으로 인해서 참조가 아닌 값이 되는 것도 맞고요.

 

5-3. 가위바위보 게임 만들기

깃 링크 : https://github.com/Harimad/zeroReact/commit/0d372d5e7ef327e5d0dec8ef721117b818ef2568

자세한 Comment  참고


Object.entries : https://devdocs.io/javascript/global_objects/object/entries

const object1 = {
  a: 'somestring',
  b: 42
}

console.log(Object.entries(object1)) 
// (2) [Array(2), Array(2)]
// 0: (2) ['a', 'somestring'], 1: (2) ['b', 42]

 

computerChoice 함수 로직 이해

const rspCoords = {
  바위: '0',
  가위: '-142px',
  보: '-284px',
}
class RSP extends Component {
  state = {
    result: '',
    imgCoord: rspCoords.바위,
    score: 0,
  }
  const computerChoice = imgCoord => {
    // 콘솔 확인
    console.log(Object.entries(rspCoords))	// (3) [Array(2), Array(2), Array(2)]
    console.log(Object.entries(rspCoords).find(v => v[1] === imgCoord)) // 화면에 맞늦 결과가 출력 // ['보', '-284px']
    console.log(Object.entries(rspCoords).find(v => v[1] === imgCoord)[0]) // '보'
    return Object.entries(rspCoords).find(v => v[1] === imgCoord)[0] // '보' 리턴
  }
  onClickBtn = choice => () => {
    const { imgCoord } = this.state
    const cpuScore = scores[computerChoice(imgCoord)]
  }
}

 

5-4. 고차 함수와 Q&A 🎁

리액트에서 많이 하는패턴 (high order function - 고차함수)

 

Q1. const name = () => () => {} 의 작동 원리가 어떻게 되는지 궁금합니다 !

또 그냥 function 으로는 어떤 식으로 표현하죠?? 

 

->  고차함수입니다. function으로 만들면

function() {
  return function() {}
}

입니다. 함수가 함수를 리턴하면 됩니다.

 

 

Q2. onClickBtn()는 왜 한번 더 함수를 쓰는지 이해가 안되요...

<button id="rock" className="btn" onClick={() => this.onClickBtn('바위')}>바위</button>

() => 를 빼면 랜더링 에러가 나는데.. 
왜 그런건지 설명좀 해주세요  () =>  왜 들어가는지 이해가 안되요 

-> this.onClickBtn('바위') 이것은 함수일까요 아닐까요?
함수가 아닙니다. 함수를 실행한 결과물입니다.
onClick에는 함수를 넣어야 합니다.

아래처럼 함수를 실행한 결과물만 넣으려면
<button id="rock" className="btn" onClick={this.onClickBtn('바위')}>바위</button> 

onClickBtn = (choice) => { }
에서
onClickBtn = (choice) => () => { }
로 바꿔 줘야한다.

❗그러면 로직은 이렇게된다.
버튼 누름 
-> onClick 메서드 발동 
-> this.onClickBtn 함수에 인자로 '바위'가 들어감. //  onClickBtn = (choice) <- '바위'
-> 그런데 onClick 메서드에는 함수가 들어가야함.
-> 그래서 화살표함수가 나오도록 한다. // onClickBtn = (choice) => () => { } <- 인자를 받고, 이제 화살표 함수 호출!!


 

Q2-1. render() 에서는 아래처럼 되어있다.

onClick 메서드 안에 함수를 호출 하는 부분이 있다. 이걸 간단하게 하는 방법?

<button onClick = { () => this.onClickBtn('바위')} >바위</button>

<!--onClickBtn 함수는 아래처럼 되어있다.-->

onClickBtn = choice => {
	...
}

render 태그 안에 onClick 함수를 직접 넣지말고
this.함수 이렇게 직접 호출하는 방식이 좋다.

여기서 render 태그안에서 onClick 메서드 안에 함수를 호출하는 방식으로 하지말고
함수를 바로 호출하는 방법으로 하려면 아래처럼 화살표를 빼면된다.

 

<button onClick = { this.onClickBtn('바위') }바위</button>

<!--그러면 호출 되는 함수안에는 화살표를 다시 붙여준다. -->
onClickBtn = choice => () => {
	...
}

 

Q2-2❗❗.  onClickBtn = (choice) => () => {} 라고하면 에러가안나고
onClickBtn = (choice) => {} 라고하면 에러가납니다. (too many rerender, infinate loop ERROR)
고차함수 안쓰면 위의 코드를 쓰면, 버튼을 누르지도 않았는데 onClickBtn을 호출합니다.

콘솔찍어보니 나오더군요. 계속 계속 호출하더라구요. 왜 이런가요?

 

-> 고차함수를 안 쓰면 onClickBtn() 하는 순간, 클릭을 안 했는데도 바로 호출되니까 그렇습니다. 
그래서 고차함수를 써서 리턴에 함수를 넣는거고요.
onClick은 함수 자리입니다. onClickBtn()은 함수가 아닙니다. return값입니다. 고차함수여야 return값이 함수가 됩니다.


Q2-3. 이 강의에서는 onClcik을 사용할때 onClick={()=>this.onClickBtn}  으로 사용하고 

저번 강의때는 onClick={this.onClickScreen} 로 사용하는데 차이가 무엇인가요 ??
왜 이 강의에서는 ()=>를 한번 더 거쳐서 가야하는지 이해가 안갑니다. 

저번 강의들은 바로 함수를 호출하는데 이번 강의에서는 함수 안에서 다시 함수를 호출하는이유가 궁금합니다.

 

-> 별도의 매개변수가 없을 때는 onClick={this.onClickBtn}만 하면 됩니다.

 

Q2-3-a. 그럼 매개변수 사용할때 고차함수를 사용해야한다는 말씀이신가요 ?? 
매개변수가 있을 때 onClick={this.onClickBtn} 을 사용하니 react-dom.development.js:2525 Uncaught Error: 

Too many re-renders. React limits the number of renders to prevent an infinite loop 오류가 발생하더라구요 .
이에대한 개념이 잘 안잡혀있어서 이해가 잘 안가네요...

 

->네. 매개변수를 사용해야 하는 경우 this.onClickBtn(매개변수) 이렇게 넣어야합니다. 

선언은 onClickBtn = (매개변수) => (이벤트) => {}가 되는 거고요.

 

Q2-3-b. 그럼 버튼을 onClick 했을 때 이벤트를 제어하기 위해 고차함수를 사용한다고 이해하면 될까요 ??
onClickBtn = (매개변수) => (이벤트) => {} 에서 받은 매개변수와 이벤트를 {} 안에서 제어한다?? 

이런식으로 이해하면될까요 ??

 

-> 이벤트는 고차 함수 안 써도 기본적으로 들어있고요. 보통 매개변수를 받아서 쓰기 위해서 고차함수를 씁니다.


❗Q3. 성격급한사람은 가위바위보 게임할때 결과 보여주는 대기시간에 마구 클릭해서

interval 가 계속쌓여 속도가 빨라지는데 setTimeout() 종료되기전까지 클릭 막을려면 어케해야되나요?

 

-> useRef에다가 클릭 가능 여부를 판단하는 불 값을 넣어두시면 됩니다.

클릭하면 false로 두고 setTimeout끝나면 true로 바꾸고요. 클릭하는 곳에서는 이 값으로 판단하면 됩니다.

 

5-5. Hooks와 useEffect

1. useEffect

훅스가 라이프 사이클이 없지만
클래스 라이프 사이클을 흉내 낼 수 있다.

그럼

ComponentDidmount랑
ComponentWIllUnmount 처리는 어떻게 할까?

-> 여기서 useEffect라는 개념이 나옴 (핵심)

useEffect ( () => // componentDidMount(두번째인자가 []일때, 1번실행), componentDidUpdate 역할 (1대1 대응은 아님)
    return () => {  //componentWillUnmount 역할
      //..
  }
}, [] );

 

2. 이 상태로 놓고 코드를 실행시켜보면 제대로 실행이 안된다.

  const changeHand = () => {
    if (imgCoord === rspCoords.바위) {
      setImgCoord(rspCoords.가위)
    } else if (imgCoord === rspCoords.가위) {
      setImgCoord(rspCoords.보)
    } else if (imgCoord === rspCoords.보) {
      setImgCoord(rspCoords.바위)
    }
  }

useEffect(() => {
  console.log('시작')
  interval.current = setInterval(changeHand, 1000)
  //componentWillUnmount 역할
  return () => {
    console.log('종료') 
    clearInterval(interval.current)
  }
}, [])

useEffect() 의 두번째 인자가 빈 배열 이기 때문에 setInterval(changeHand, 1000)이 한번 실행이 된다.

그러면 changeHand 함수만 1초 마다 실행이된다. 클로저 문제 때문에 imgCoord값이 고정된 '바위'값만 가져온다.

그래서 '바위' -> '가위' 상태가 유지된다.

그렇다면 imgCoord값이 '바위' -> '가위' -> '보' -> '바위' -> ... 이처럼 정상적으로 작동하게 하려면 어떻게 해야할까?

 

useEffect()의 두 번째 인자배열 값에 imgCoord를 넣는다. 이러면 useEffect 안의 코드가 재실행된다.

그러면 setInterval 함수가 재실행 되면서 changeHand함수가 1초마다 실행되고 imgCoord 값이 정상적으로 바뀌게 된다.

 

 

3. 한가지 더 눈여겨 볼점은 useEffect()가 console.log('시작')과 console.log('종료')가 번갈아서 출력된다는 점이다.

즉, setInterval이 시작되자마자 종료가 된다는 것이다.

이는 매번 clearInterval을 하기 때문에 그냥 setTimeout을 하는 것과 동일하다고 보면 된다.

 

 


Q. background 옵션이 어떻게 되는건가요?

background: `url(https://en.pimg.jp/023/182/267/1/23182267.jpg) ${imgCoord} 0`

앞 두개는 background-image랑 background-position ? 같은데 

마지막 0은 어떤건지 궁금합니다

-> 0도 백그라운드 포지션입니다. ${imgCoord}가 left고 0이 top 속성입니다.


 

 

5-6. 클래스와 Hooks 라이프사이클 비교


1. class의 경우 componentDidMount 나 componentDidUpdate에서
모든 state를 조건문으로 분기 처리한다.

2. useEffect() 를 쓸때 배열에는 꼭 useEffect를 다시 실행할 값(관련있는 값만 넣기)만 넣어야 한다.

3. React.memo를 쓰는 이유?
자식 컴포넌트에서 props 값이 바뀌면 부모 컴포넌트도 계속 리랜더링이 일어난다.
부모 컴포넌트에서 컴포넌트 부분에 memo로 감싸주면 리랜더링을 막을 수 있다.

4. useLayoutEffect 기능?
useEffect는 화면이 다 바뀌고(render 후)나서 실행이 되는 녀석이다.
useLayoutEffect는 layout의 변화를 감지할 떄 실행되는 녀석이다.
즉, 화면이 바뀌기 전(render 전)에 실행된다.
쓸일이 거의 없다.

5. (class vs 함수 컴포넌트 의 LifeCycle의 관점에서) useEffect 쉽게 이해하는 방법 🎁
// result, imgCoord, score 값을
// componentDidMount, componentDidUpdata, componeneWillUnmount 에 해당하는 처리를 하는 방법

 


Q. ComponentDidUpdate질문

ComponentDidUpdate 호출조건 중 하나가
setState로 State값이 바뀌었을때 componentDidUpdate가 발생하는걸로 알고 있는데 componentDidUpdate안에 다시 setState를 사용하면 
setState -> componentDidUpdate -> setState -> componentDidUpdate ....... 계속해서 반복 호출이 되는게 아닌가요????

-> 네 무한 반복됩니다. 그런 무한 반복은 피하셔야겠죠?


Class형 컴포넌트 완성 코드

더보기
import React, { Component } from 'react'

//클래스의 경우 - 컴포넌트 Life Cycle
//1. constructor
//2. 처음 render() 실행
//3. render속 ref 설정
//4. compoenentDidMount() 한번 실행
//5. setState/props 바뀔때 -> shouldComponentUpdate(true) -> render() -> componentDidUpdate()
//6. 부모가 나를 없앴을 때 -> componentWillUnmount() -> 소멸

const rspCoords = {
  바위: '0',
  가위: '-142px',
  보: '-284px',
}

const scores = {
  가위: 1,
  바위: 0,
  보: -1,
}

const computerChoice = imgCoord => {
  // 콘솔 확인
  // console.log(Object.entries(rspCoords))
  // console.log(Object.entries(rspCoords).find(v => v[1] === imgCoord))
  // console.log(Object.entries(rspCoords).find(v => v[1] === imgCoord)[0])
  return Object.entries(rspCoords).find(v => v[1] === imgCoord)[0]
}

class RSP extends Component {
  // 1
  state = {
    result: '',
    imgCoord: rspCoords.바위,
    score: 0,
  }

  interval

  // 3. 컴포넌트가 첫 렌더링된 후, 여기에 비동기 요청을 많이 해요
  componentDidMount() {
    this.interval = setInterval(this.changeHand, 1000)
  }

  // shouldComponentUpdate() {} // 5. return true 일 경우

  // componentDidUpdate() {} // 6. 리랜더링 후

  // 7. 컴포넌트가 제거되기 직전, 비동기 요청 정리를 많이 해요
  componentWillUnmount() {
    clearInterval(this.interval)
  }

  changeHand = () => {
    const { imgCoord } = this.state
    if (imgCoord === rspCoords.바위) {
      this.setState({ imgCoord: rspCoords.가위 })
    } else if (imgCoord === rspCoords.가위) {
      this.setState({ imgCoord: rspCoords.보 })
    } else if (imgCoord === rspCoords.보) {
      this.setState({ imgCoord: rspCoords.바위 })
    }
  }

  onClickBtn = choice => () => {
    const { imgCoord } = this.state
    clearInterval(this.interval)
    const myScore = scores[choice]
    const cpuScore = scores[computerChoice(imgCoord)]
    const diff = myScore - cpuScore
    if (diff === 0) {
      this.setState({
        result: '비겼습니다',
      })
    } else if ([-1, 2].includes(diff)) {
      this.setState(prevState => {
        return {
          result: '이겼습니다',
          score: prevState.score + 1,
        }
      })
    } else {
      this.setState(prevState => {
        return {
          result: '졌습니다',
          score: prevState.score - 1,
        }
      })
    }
    this.timeout = setTimeout(() => {
      this.interval = setInterval(this.changeHand, 1000)
    }, 2000)
  }

  // 2, 4(setState/props바뀔때)
  render() {
    const { result, imgCoord, score } = this.state
    return (
      <>
        <div
          id="computer"
          style={{
            background: `url(https://en.pimg.jp/023/182/267/1/23182267.jpg) ${imgCoord} 0`,
          }}
        ></div>
        <div>
          <button id="rock" className="btn" onClick={this.onClickBtn('바위')}>
            바위
          </button>
          <button
            id="scissor"
            className="btn"
            onClick={this.onClickBtn('가위')}
          >
            가위
          </button>
          <button id="paper" className="btn" onClick={this.onClickBtn('보')}>
            보
          </button>
        </div>
        <div>{result}</div>
        <div>현재 {score}점</div>
      </>
    )
  }
}

export default RSP

함수형 컴포넌트 완성 코드

더보기
import React, { useEffect, useRef, useState } from 'react'

const rspCoords = {
  바위: '0',
  가위: '-142px',
  보: '-284px',
}

const scores = {
  가위: 1,
  바위: 0,
  보: -1,
}

const computerChoice = imgCoord => {
  return Object.entries(rspCoords).find(v => v[1] === imgCoord)[0]
}

const RSP = () => {
  const [result, setResult] = useState('')
  const [imgCoord, setImgCoord] = useState(rspCoords.바위)
  const [score, setScore] = useState(0)
  const interval = useRef()

  const changeHand = () => {
    if (imgCoord === rspCoords.바위) {
      setImgCoord(rspCoords.가위)
    } else if (imgCoord === rspCoords.가위) {
      setImgCoord(rspCoords.보)
    } else if (imgCoord === rspCoords.보) {
      setImgCoord(rspCoords.바위)
    }
  }

  // componentDidMount(두 번째 인자가 []일때), componentDidUpdate 역할 (1대1 대응은 아님)
  useEffect(() => {
    console.log('시작')
    interval.current = setInterval(changeHand, 1000)
    //componentWillUnmount 역할
    return () => {
      console.log('종료')
      clearInterval(interval.current)
    }
  }, [imgCoord])

  const onClickBtn = choice => () => {
    clearInterval(interval)
    const myScore = scores[choice]
    const cpuScore = scores[computerChoice(imgCoord)]
    const diff = myScore - cpuScore
    if (diff === 0) {
      setResult('비겼습니다')
    } else if ([-1, 2].includes(diff)) {
      setResult('이겼습니다')
      setScore(prevState => prevState + 1)
    } else {
      setResult('졌습니다')
      setScore(prevState => prevState - 1)
    }
    setTimeout(() => {
      interval.current = setInterval(changeHand, 1000)
    }, 2000)
  }

  return (
    <>
      <div
        id="computer"
        style={{
          background: `url(https://en.pimg.jp/023/182/267/1/23182267.jpg) ${imgCoord} 0`,
        }}
      ></div>
      <div>
        <button id="rock" className="btn" onClick={onClickBtn('바위')}>
          바위
        </button>
        <button id="scissor" className="btn" onClick={onClickBtn('가위')}>
          가위
        </button>
        <button id="paper" className="btn" onClick={onClickBtn('보')}>
          보
        </button>
      </div>
      <div>{result}</div>
      <div>현재 {score}점</div>
    </>
  )
}

export default RSP

 

댓글
다크모드
Document description javascript psychology
더보기 ,제목1 태그 호버