Skip to content

Latest commit

 

History

History
480 lines (431 loc) · 16.7 KB

20210717.md

File metadata and controls

480 lines (431 loc) · 16.7 KB

React

인상 깊은 조언

  • 직장을 잡을 때 고려할 사항
    • 프론트엔드 엔지니어가 몇 명이 있는지
    • 프론트와 백엔드가 확실히 나뉘어져 개발을 하는 회사인지
    • 서비스가 내가 사용하고 싶을 정도로 괜찮은지
    • 집에서 너무 멀지는 않은지
    • 연봉과 복지가 너무 심하게 낮은 것은 아닌지
    • 개발문화가 괜찮은 회사인지

배열 렌더링

  • Cats.js에서 여러개 객체(cat objects)가 들어있는 배열을 정의한 후, li 태그 하나하나를 ul 태그가 인식하도록 전달하였다.
import { Component } from 'react'

const cats = [{ id: 1, name: 'cat1', breed: 'Korean Shorthair' }, ... ]

Class Cats extends Component {
  constructor(props) {
    super(props)
    this.state = {
      cats	# key, value의 이름이 같으면 키만 작성해도 된다.
    }
  }
  render() {
    return (
      <ul>
        { cats.map(cat => (
          <li> 
	    Name: <span>{cat.name}</span> 
	    Breed: <span>{cat.breed}</span>
	  </li>
	)) }
      </ul>
    )
  }
}

Key Props 전달

  • React에서는 key가 없는 배열 렌더링을 하면 배열의 index를 key로 삼는다.
  • 배열이 동적으로 바뀌는 경우 (고양이가 추가되면) 컴포넌트에 매핑되어있는 배열의 index가 바뀌므로 효율적 관리가 어렵다.
  • 배열이 앱의 라이프사이클 상 바뀌지 않는다고 한다면 배열 index를 key로 사용해도 되지만 그렇지 않은 경우를 위해 key props를 전달한다.
  • Key Prop을 unique한 값으로 지정하면 렌더링은 다시 되지만 해당 키를 가진 애들은 돔 구조가 바뀌지 않고, 변경이 있는 애들만 변경된다. 즉, 성능을 좋게 만든다.
  • 우리는 고양이의 이름과 id값을 조합한 key props를 전달하자.
class Cats extends Component {
  // 중략
  render() {
    return (
      <ul> {cats.map(cat => (
	<li key={`${cat.id}-${index}`}>
	  // 중략
	</li>
      ))}
      </ul>
    )
  }  
}

Life Cycle Method

  • class component에서 사용한다.
    • 복잡해서 최근 현업에서는 많이 사용되지 않는 편이다.
  • 컴포넌트가 브라우저에 만들어지고(mount), 업데이트되고(update), 사라지거나(unmount) 에러가 나는 등 생애주기마다 React가 호출해주는 함수
  • 이후 배울 function component에서 사용하는 hooks, useEffect가 하는 역할과 흡사
  • renderconstructor도 life cycle method 중 하나이다.

주요 생애주기와 호출되는 Life Cycle Method

Mounting

  • Virtual DOM에 만들어진 컴포넌트가 실제 DOM에 반영될 때
  • constructor
    • 클래스 컴포넌트가 생성될 때 실행, 처음 만들어질 때 딱 한 번만 실행
  • render
    • 렌더링하는 메서드
  • componentDidMount
    • 컴포넌트가 처음 만들어지고 렌더링이 끝나고 나면 호출되며, 호출되는 시점은 이미 화면에 컴포넌트가 그려진 시점이다.
    • API를 호출하여 서버에서 데이터 불러올 때 가장 많이 쓴다.
    • 만들어진 DOM에 접근하는 등의 작업에도 사용된다. 컴포넌트에 input 등의 태그가 가지고 있어서 DOM이 형성되어 화면에 그려진 이후 해당 요소를 취득해야 하는 경우 등

Update

  • shouldComponentUpdate
    • 컴포넌트 리렌더링 여부를 결정. true를 반환하면 리렌더링을 하는데, false를 반환하게 하면서 필요없는 render를 트리거하지 않음으로써 성능을 향상시킨다.
    • props, state 인자 2개를 받는데, 이는 업데이트된 후의 props, state를 가리킨다.
    • nextPropsnextState로 매개변수를 명명하고 두 인자를 비교하여 변동이 없으면 false를 반환하여 리렌더링을 방지할 수 있다.
shouldComponentUpdate(nextProps, nextState) {
  if (nextProps.name === nextState.name){
    return false
  }
  return true
}
  • componentDidUpdate
    • 업데이트 후, 리렌더링이 완료되고 화면에 변화가 이루어진 후에 호출
    • prevProps, prevState, snapShot 세 개의 인자를 받으며 현재 상태는 this.props로 참조할 수 있다.

Unmount

  • 컴포넌트가 Virtual DOM과 실제 DOM에서 사라질 때
  • componentWillUnmount
    • 컴포넌트가 사라지기 직전에 호출
    • DOM에 달아놓은 이벤트리스너 제거
    • setTimeOut이나 setInterval을 해제해주는 작업을 해주는 경우가 많다.

Error

  • componentDidCatch
    • 컴포넌트 내부에서 에러가 발생했을 때 호출된다.

실습

API로부터 정보 받아와서 렌더링

  • 고양이 사진 및 정보를 API로 받아와서 class component로 렌더링하며 Life Cycle Method를 사용해보자
  • class 안에 componentDidMount 함수를 정의하되, API에서 fetch로 데이터를 받을 거니까 async 키워드를 함수 앞에 넣어준다.
class Cats extends Component {
  constructor(props) {
    super(props)
    this.state = {
      currentPage: 1,	// API에서 받아올 페이지를 1로 초기화한다.
      breeds: []	// API에서 받아올 데이터를 넣을 빈 배열
    }
  }
  
  async componentDidMount() {
    console.log('start fetching')
  }
}
  • API 키를 받아 fetch 함수를 작성한다.
    • 이 때 첫 번째 인자는 url이며 page와 limit를 param으로 준다.
    • 두 번째 인자는 API 키를 담은 headers가 들어있는 객체
  async componentDidMount() {
    const response = await fetch('https://api.thecatapi.com/v1/breeds?page=${currentPage}&limit=3', 
      {
      	headers: {
	  'x-api-key': apiKey
	}
      })
    const breeds = await response.json()
    this.setState({
      breeds,
    })
  }
  • fetch로 받은 고양이 정보를 response로 받고, json 형태로 바꾼 후 setState로 breeds에 넣어준다.

page 버튼 만들기

  • 버튼 누르면 다음 페이지의 정보를 받아오게끔 constructor state에 1로 초기화했던 currentPage를 바꿔가며 호출한다.
  • 현재 렌더링하고 있는 태그가 ul인데, 렌더함수는 하나의 태그만 리턴할 수 있으므로 div로 씌운 후 ul 위에 onclick 속성을 준 버튼태그를 마크업해준다.
  render() {
    return (
      <div>
      <button onclick= () => {
	  this.setState((prevState) => {
	    return { currentPage : prevState.currentPage -1 }
	  })
	} > 이전 페이지 </button>
      <button onclick= () => {
	  this.setState((prevState) => {
	    return { currentPage : prevState.currentPage +1 }
	  })
	} > 다음 페이지 </button>
      </div>
    )
  }
  • render 함수 상단에 다음 페이지에서 데이터를 가져올 componentDidUpdate 함수를 작성한다.
    • fetch로 가져올 거니까 asyncawait 사용해주기
  render() {
  async componentDidUpdate(prevProps, prevState) {
    if (prevState.currentPage === this.state.currentPage) {
      return	// 변화가 없다면 그냥 넘어간다.
    }
    const response = await fetch('https://api.thecatapi.com/v1/breeds?page=${prevState.currentPage}&limit=3', 
      {
      	headers: {
	  'x-api-key': apiKey
	}
      })
    const breeds = await response.json()
    this.setState({
      breeds: prevState.breeds.concat(breeds),
    })
  } 

utils로 반복되는 함수 빼주기

  • fetch로 가져오는 것이 반복되니까, utils 폴더 속 api.js 파일을 생성하여 빼준다.
  • page를 매개변수로 받고 인자로 들어오는 값이 number인지 validate해준다.
const apiKey = '{API KEY값}'

export const getCatBreeds = async (currentPage) => {
  if (typeof currentPage !== 'number') {
    throw new Error(
      'getCatBreeds 함수의 currentPage 파라미터는 Number이어야 합니다.'
    )
  }
  const response = await fetch(
    'https://api.thecatapi.com/v1/breeds?page=${currentPage}&limit=3',
    {
      headers: {
        'x-api-key': apiKey
      }
    }
  )
  const breeds = await response.json()
  return breeds
}
  • component 파일로 돌아와 import해준다.
import { getCatBreeds } from '../utils/api'

Loading Indicator 만들기

  • 첫 렌더링과 update하는 함수에서 Fetch 가 완료되어 렌더링할 때까지 loading indicator를 띄워준다.
  • 사용성에 따라 방법과 위치는 달라질 수 있지만, 우리는 버튼 태그 뒤에 넣어주는 것으로 하자.
  • constructor에서 state에 isLoading 변수를 false로 초기화해준다.
  • componentDidMount 함수의 처음, componentDidUpdate 함수의 처음에 setState로 isLoading값을 true로 변경해준다.
  • API로 데이터를 받아와 breeds를 state에 넣어줄 때 isLoading도 false로 값 변경해준다.
  constructor(props) {
    super(props)
    this.state = {
      currentPage: 1,
      breeds : [],
      isLoading : false
    }
  }

  async componentDidMount() {
    this.setState({
      isLoading : true
    })
    const breeds = await getCatBreeds(this.state.currentPage)
    this.setState({
      breeds,
      isLoading: false
    })
  }

  async componentDidUpdate(prevPage, prevState) {
    if (prevState.currentPage === this.state.currentPage){
      return
    }
    this.setState({
      isLoading : true
    })
    const nextBreeds = await getCatBreeds(this.state.currentPage)
    this.setState({
      breeds: prevState.breeds.concat(nextBreeds),
      isLoading: false
    })
  }

  render() {
    return (
      <div>
	<button {버튼태그 관련 생략}>이전페이지/다음페이지</button>
	{this.state.isLoading && <span>로딩중...</span>}  
        <ul>
	  {this.state.breeds.map((breeds, index) => (<li>{고양이정보들..}</li>)}
	</ul>
      </div>
    )
  }

Component로 빼주기

  • Loading Indicator을 아예 component로 뽑아내서 import하여 사용할 수도 있다.
import { Component } from 'react'

class LoadingIndicator extends Component {
  constructor(props) {
    super(props)
    if (typeof this.props.isLoading !== 'boolean') {
      throw new Error('isLoading props가 전달되지 않았습니다.')
    }
  }
  render() {
    if (!this.props.isLoading) {
      return null
    }
    return <span>로딩중...</span>
  }
}

export default LoadingIndicator
  • import하여 사용할때는 아래와 같이 사용한다.
import LoadingIndicator from './LoadingIndicator.js'

  render() {
    return ( // 앞뒤다생략
        <LoadingIndicator isLoading={this.state.isLoading} />
    )
  • Button들도 component로 빼서 사용할 수 있다.
    • 이 때 button 태그의 onclick 이벤트핸들러는 상위 컴포넌트에 메서드로 정의해줘야 한다.
class Cats extends Component {
  constructor(props) {
    super(props)
    this.state = {
      currentPage : 1,
      breeds : [],
      isLoading : false
    }
  }

  handlePrevPage = () => {
    if (this.state.currentPage <= 1) {
      return
    }
    this.setState(prevState => ({
      currentPage: prevState.currentPage - 1
    }))
  }

  handleNextPage = () => {
    this.setState(prevState => ({
      currentPage: prevState.currentPage + 1
    }))
  }
}
  • prevState의 currentPage 값을 바꾸는 함수를 props에서 받아 onClick 속성 값으로 갖는 button 태그를 렌더하는 컴포넌트를 만든다.
  • 함수 값이 넘어오는지 validation 해주기
import { Component } from 'react'

class ButtonGroup extends Component {
  constructor(props) {
    super(props)
    if (
      typeof this.props.onPrevPage !== 'function' ||
      typeof this.props.onNextPage !== 'function'
    ) {
      throw new Error(`함수데려와`)
    }
  }

  render() {
    return (
      <>
        <button onClick={this.props.onPrevPage}>이전페이지</button>
        <button onClick={this.props.onNextPage}>다음페이지</button>
      </>
    )
  }
}

export default ButtonGroup
  • button 컴포넌트를 데려와서 onPrevPage/onNexPage 속성에 상단에 정의해둔 handle 함수들을 전달해준다.
class Cats extends Component {
  constructor(props) {
    ...
  }
  
  handlePrevPage = () => { ... }
  handleNextPage = () => { ... }

  render() {
    return (
      <div>
        <ButtonGroup 
	  onPrevPage={this.handlePrevPage} 
	  onNextPage={this.handleNextPage} 
	/>
    )
  }
  • 이로써 컴포넌트 안에서도 컴포넌트를 import 하여 사용할 수 있다.
    • 다만 props를 잘 넘겨주는 것이 필요하다.

Function Component & Hooks

Class component vs. Function component

  • class component에서는 render 함수가 JSX 문법으로 된 태그를 리턴했지만, function component 에서는 return을 바로 해버린다.
  • 상태관리와 life cycle method에서 차이가 난다.
    • class component: this.statesetState 메서드로 상태를 관리하며, componentDidMount 등으로 life-cycle마다 호출되는 메서드를 사용
    • function component는 useState로 상태를 관리하며, useEffect라는 단 하나의 Hooks로 관리
  • class component의 getDerivedStateFromProps 메서드가 필요없고, 함수의 파라미터가 props이며 그 props를 그대로 useState에 사용
  • class component의 shouldComponentUpdate 메서드는 memo로 대체하여 구현한다.

자주 쓰는 Hooks

  • useState, useMemo: 비슷한 역할
    • useMemo: 상태 중에 값비싼(여러번 실행되면 상태 꼐산으로 인해 성능을 떨어뜨리는) 경우에 memoization기법을 사용한다.
  • useEffect, useLayoutEffect
    • useEffect: 안에 함수를 넣을 수 있으며, 렌더링 이후 실행되는 componentDidMountcomponentDidUpdate를 대체한다.
    • useLayoutEffect: 동작시점이 다소 다르다. 렌더링 후 DOM 구조가 실제로 변경된 직후 동기적으로 실행된다.

Function Component에서의 상태관리

  • useState: 일반적으로 우리가 생각하는 간단한 상태를 정의
import { useState } from 'react'

function App() {
  return (
    <Counter />
  )
}


function Counter() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1)
  }
  
  return (
    <section>
      <span>{count}</span>
      <button onClick={handleClick}>Plus 1</button>
    </section>
  )
}
  • useState는 배열을 반환하는데, 첫 번째 요소는 관리할 상태이고, 두 번째 요소는 그 상태를 업데이트 할 수 있는 setter함수이다.
  • useState의 파라미터에 넘기는 값은 관리할 상태의 초기값이 된다. (여기서는 count에 0이라는 초기값이 들어간다.)
    • 넘기지 않는 경우 undefined가 초기값으로 넘어간다.
  • class component였다면 state라는 객체를 만든 후 property로 넣었어야 하는 과정.
    • setState는 리터럴 객체를 받아 원래 객체에 병합하는 merge방식으로 상태를 업데이트한다.
  • useState는 override 방식으로 상태를 업데이트하는데, 객체를 병합하는 것이 아니라 setter함수가 받은 값으로 상태를 바꿔치기 한다.
    • 우리의 관심사인 값 하나를 count라는 상수 하나로 바로 꺼내서 사용이 가능
  • setter함수는 값을 받을 수도, 함수를 받을 수도 있다.
    • 값을 받으면 비동기적으로 해당 상태를 업데이트
    • 함수를 받으면 그 함수의 첫 번째 parameter로 이전 상태를 넣어준다.

Lazy Initial State

  • useState에 인자로 넣으면 첫 렌더링 시의 초기상태로 지정된다.
    • functional component에서는 동작이 일어나면 함수가 통째로 재실행된다.
    • c.f. class component에서는 해당하는 메서드만 실행 후 render이 됨
    • useState에 주었던 초기값은 재실행되는 경우에는 무시된다.
  • 비싼 연산을 하는 getCountuseState에 호출하면서 인수로 넣으면 리렌더링 될때마다 쓸데없이 연산을 한다.
import { useState } from 'react'

function getCount() {
  return Array(100000).fill(1).reduce((acc, cur) => acc+cur, 0)
}

function App() {
  return ( <Counter /> )
}

function Counter() {
  const expensiveCalculation = getCount()
  const [count, setCount] = useState(expensiveCalculation)
  const handleClick = () => {
    setCount((previousCount) => previouseCount + 1)
  }

  return (<section> ... </section>)
}
  • 이를 해결하기 위해서는 useState에 인자로 콜백함수를 넘긴다.
function Counter() {
  const [count, setCount] = useState( () => getCount() )
  // ... 생략
}
  • 이 경우 React는 함수가 반환한 값을 한번만 계산하여 사용하고 그 뒤에 일어나는 렌더링 때는 계산하지 않는다.

느낀 점

  • 조금씩 조금씩 사용법을 익히는 중... 어렵지만 차근히 해보자.