리액트

#6. Component 개선하기(1) - Component 생애주기(lifecycle)

YJH3968 2021. 3. 30. 15:39
728x90

1. 마운팅 생애주기

  • component 생애주기는 component가 마운트되거나 갱신될 때 호출되는 일련의 method로 이루어진다.
  • 이런 method는 component가 UI에 렌더링되기 전이나 후에 호출된다. render method 자체도 component 생애주기의 일부다.
  • 마운팅 생애주기와 갱신 생애주기가 있다.
  • 마운팅 생애주기는 component가 마운트되거나 언마운트되면 호출되는 method다.
  • 최초 상태를 설정하거나, API를 호출하거나, 타이머를 시작 또는 종료하거나, 서드파티 라이브러리를 초기화하는 등의 작업에 이런 method를 사용할 수 있다.
  • React.createClass를 사용하는 경우 getDefaultProps가 먼저 호출되어 component의 프로퍼티를 얻고, 그 후 getInitialState를 호출해 상태를 초기화한다.
  • 반면에 ES6 클래스에는 이런 method가 없고 대신 생성자의 인자로 프로퍼티가 전달된다. 생성자 안에서 상태를 초기화해야 한다.
  • ES6 클래스 생성자나 getInitialState 모두 프로퍼티에 접근할 수 있으므로 component의 초기 상태를 정의할 때 필요하면 프로퍼티 값을 활용할 수 있다.
  • 다음은 component 마운팅 생애주기와 관련있는 method를 나타내는 표다. 
ES6 클래스 React.createClass()
  getDefaultProps()
constructor(props) getInitialState()
componentWillMount() conponentWillMount()
render() render()
componentDidMount() componentDidMount()
componentWillUnmount() componentWillUnmount()
  • 프로퍼티를 얻고 상태를 초기화하면 componentWillMount method가 호출되고 이 method는 DOM이 렌더링되기 전에 호출돼 서드파티 라이브러리를 초기화하거나, 애니메이션을 시작하거나, 데이터를 요청하거나, component를 렌더링하기 전에 설정해야 하는 단계를 추가 실행하기 위해 사용할 수 있다.
  • 이 method 안에서 setState를 호출해서 component가 최초로 렌더링되기 직전에 component의 상태를 바꿀 수도 있다.
//  randomuser.me로부터 임의의 멤버 리스트를 받는 getFakeMembers promise
const getFakeMembers = count => new Promise((resolves, rejects) => {
  const api = `https://api.randomuser.me/?nat=US&results=${count}`
  const request = new XMLHttpRequest()
  request.open('GET', api)
  request.onload = () => (request.status == 200) ?
    resolves(JSON.parse(request.response).results) :
    rejects(Error(request.statusText))
  request.onerror = error => rejects(err)
  request.send()
})
  • 이 promise를 MemberList component에 있는 componentWillMount method에서 사용한다.
// Member component
const Member = ({ email, picture, name, location}) =>
  <div className="member">
    <img src={picture.thumbnail} alt="">
    <h1>{name.first} {name.last}</h1>
    <p><a href={"mailto"+email}>{email}</a></p>
    <p>{location.city}, {location.state}</p>
  </div>

// MemberList Component
class MemberList extends Component {

  constructor() {
    super()
    this.state= {
      members: [],
      loading: false,
      error: null
    }
  }

  componentWillMount() {
    this.setState({loading:true})
    getFakeMembers(this.props.count).then(
      members => {
        this.setState({members, loading: false})
      },
      error => {
        this.setState({error, loading: false})
      }
    )
  }

  componentWillUpdate() {
    console.log('갱신 생애주기')
  }

  render() {
    const { members, loading, error } = this.state
    return (
      <div className="member-list">
        {(loading) ?
          <span>멤버 로딩 중</span> :
          {members.length} ?
            members.map((user, i) =>
              <Member key={i} {...user} />
          ) : 
          <span>0 멤버 로딩됨...</span>
        }
        {(error) ? <p>멤버 로딩 오류: {error}</p> : ""}
      </div>
    )
  }
}
  • component가 렌더링되기 전에 setState를 호출하면 갱신 생애주기 method가 호출되지 않는다. 렌더링된 다음에 setState를 호출하면 갱신 생애주기 method가 호출된다.
  • 하지만 위 예제와 같이 componentWillMount method 안에서 비동기 콜백 내부에서 setState를 호출할 경우 실제 setState가 호출되는 시점은 component가 렌더링된 이후다. 그래서 갱신 생애주기 method가 호출된다.
  • componentDidMount는 component가 렌더링된 직후에 호출되고, componentWillUnmount는 component가 언마운트되기 직전에 호출된다.
  • componentDidMount는 API 요청을 시작하기 좋은 위치다. 이 method는 component가 렌더링된 다음에 호출되기 때문에 이 method 안에서 setState를 호출하면 갱신 생애주기 method가 호출되고 component가 다시 렌더링된다.
  • componentDidMount는 DOM을 사용하는 서드파티 javascript를 초기화하기에도 좋은 위치다. 예를 들어 터치 이벤트나 드래그 앤 드롭을 처리하는 라이브러리의 경우 그 라이브러리를 componentDidMount에서 시작해야 한다. 일반적으로 그런 라이브러리는 DOM이 있어야 제대로 초기화할 수 있기 때문이다.
  • componentDidMount method에서 인터벌이나 타이머 같은 백그라운드 프로세스를 시작하는 것도 좋다.
  • componentDidMount나 componentWillMount에서 시작한 백그라운드 프로세스는 componentWillUnmount에서 종료시킬 수 있다.
  • 부모가 자식 component를 제거하거나, react-dom에서 찾을 수 있는 unmountComponentAtNode 함수를 사용해 명시적으로 언마운트를 요청한 경우 component가 언마운트된다.
  • 이 함수를 루트 component나 부모 component에도 적용할 수 있는데, 이때 자식 component를 먼저 언마운트한 후 자식 component가 모두 언마운트되면 부모 component를 언마운트한다.
// unmountComponentAtNode를 사용하는 예제
import React from 'react'
import { render, unmountComponentAtNode } from 'react-dom'
import { getClockTime } from './lib'
const { Component } = React
const { target } = document.getElementById('react-container')

class Clock extends Component {

  constructor() {
    super()
    this.state = getClockTime()
  }

  componentDidMount() {
    console.log("시계 시작 중")
    this.ticking = setInterval(() =>
        this.setState(getClockTime())
      , 1000)
  }

  componentWillMount() {
    clearInterval(this.ticking)
    console.log("시계 중단 중")
  }

  render() {
    const { hours, minutes, seconds, timeOfDay } = this.state
    return (
      <div className="clock">
        <span>{hours}</span>
        <span>:</span>
        <span>{minutes}</span>
        <span>:</span>
        <span>{seconds}</span>
        <span>:</span>
        <span>{timeOfDay}</span>
        <button onClick={this.props.onClose}>x</button>
    )
  }
}

render(
  <Clock onClose={() => unmountComponentAtNode(target)} />,
  target
)

2. 갱신 생애주기

  • 갱신 생애주기는 component의 상태가 바뀌거나 부모로부터 새로운 프로퍼티가 도착한 경우에 호출되는 method다.
  • 이 생애주기를 이용해 component가 갱신되기 전에 javascript를 처리하거나 component가 갱신된 후에 DOM에 대한 작업을 수행할 수 있다.
  • 갱신 생애주기 중에 불필요한 갱신을 취소할 수도 있어 애플리케이션의 성능을 향상시키는 경우 이를 활용할 수 있다.
  • setState를 호출할 때마다 갱신 생애주기 method가 호출되기 때문에 setState를 갱신 생애주기 method 안에서 호출하면 무한 재귀 loop가 발생할 수 있다. 그러므로 componentWillReceiveProps 안에서 바뀐 프로퍼티 값에 따라 상태를 갱신할 때만 setState를 호출한다.
  • 갱신 생애주기 method
    • componentWillReceiveProps(nextProps) : 새 프로퍼티가 component에 전달된 경우 호출된다. setState를 호출할 수 있는 유일한 갱신 생애주기 method다.
    • shouldComponentUpdate(nextProps, nextState) : 갱신 생애주기의 문지기격인 method로 갱신을 막을 수 있는 술어 함수다. 이를 이용해 꼭 필요한 경우에만 component를 갱신할 수 있도록 만들 수 있다.
    • componentWillUpdate(nextProps, nextState) : component 갱신 직전에 호출된다. componentWillMount와 유사하나 렌더링이 아니라 component 갱신 전에 호출된다.
    • componentDidUpdate(prevProps, prevState) : component 갱신이 일어난 직후 render를 호출한 다음에 호출된다. componentDidMount와 유사하나 매번 갱신이 일어난 뒤에 호출된다.
import { Star, StarRating } from '../components'

export class Color extends Component {

  componentWillMount() {
    this.style = { backgroundColor: "#CCC" }
  }

  render() {
    const { title, rating, color, onRate } = this.props
    return (
      <section className="color" style={this.style}>
        // 리액트 16은 참조를 문자열이 아닌 함수로 정의하기를 권장한다.
        // 이때 이 참조를 사용하려면 this.refs._title이 아닌 this._title이라고 써야 한다.
        <h1 ref={input => this._title = input}>{title}</h1>
        <div className="color" style={{ backgroundColor: color }}></div>
        <StarRating starsSelected={rating} onRate={onRate} />
      </section>
    )
  }
}

Color.propTypes = {
  title: PropTypes.string,
  rating: PopTypes.number,
  color: PropTypes.string,
  onRate: PropTypes.func
}

Color.defaultProps = {
  title: undefined,
  rating: 0,
  color: "#000000",
  onRate: f=>f
}
  • 위 예제는 앞 장에서 만든 색 관리 앱을 일부 변경해 마운트하기 전에 Color element의 배경색으로 회색을 지정했다.
  • 여기에 componentWillUpdate를 추가해 색이 갱신되기 직전에 각 색의 회색 배경을 제거할 수 있다. 이 코드를 실행하고 색을 하나 골라 평점을 매기면 한 색에만 평점을 매기더라도 다른 Color component도 모두 갱신된다.
// componentWillUpdate()를 Color component에 추가한다.
componentWillUpdate() {
  this.style = null
}
  • 이는 부모 component인 ColorList가 상태를 갱신할 때 자식 Color component를 모두 렌더링하기 때문이다. 하나의 색에 평점을 매기면 모든 Color component, StarRating component가 갱신된다.
  • 프로퍼티 값이 바뀌지 않은 색의 갱신을 막으면 애플리케이션의 성능을 향상시킬 수 있다. 이를 위해 shouldComponentUpdate method를 이용한다. 이 method는 true나 false를 반환하는데, true인 경우 component를 갱신하고, false면 하지 않는다.
// shouldComponentUpdate(nextProps)를 Color component에 추가한다.
shouldComponentUpdate(nextProps) {
  const { rating } = this.props // 아직 갱신 전이므로 이전 프로퍼티를 this.props로 접근할 수 있다.
  return rating !== nextProps.rating
}
// 갱신 전후에 실행되는 method
componentWillUpdate(nextProps) {
  const { title, rating } = this.props
  this.style= null
  this._title.style.backgroundColor = "red"
  this._title.style.color = "white"
  alert(`${title}: 평점 ${rating} -> ${nextProps.rating}`)
}

componentDidUpdate(prevProps) {
  const { title, rating } = this.props
  const status = (rating > prevProps.rating) ? '좋아짐' : '나빠짐'
  console.log(`${title} 평점이 ${status}`)
  this._title.style.backgroundColor = ""
  this._title.style.color = black
}
  • 갱신 생애주기 method인 componentWillUpdate와 componentDidUpdate는 갱신 전후에 DOM element와 상호 작용하기 좋은 위치다. 
  • 위 예제에서 평점을 바꾸면 componentWillUpdate가 갱신 직전에 실행돼 해당 Color component의 배경색을 빨간색으로 바꾸고 알림창이 뜨면서 갱신 프로세스를 잠시 멈춘다. 이후 알림창을 닫으면 갱신이 진행돼 componentDidUpdate가 호출돼서 Color component의 배경색이 다시 원상 복귀된다.
  • 때로는 component가 맨 처음에 프로퍼티로부터 설정한 상태를 유지하기도 한다. 이러한 초기 상태는 생성자나 componentWillMount 생애주기 method 안에서 설정할 수 있다. 이때 상태를 초기화할 때 참조했던 프로퍼티가 바뀌면 componentWillReceiveProps method를 사용해 상태를 갱신할 필요가 있다.
// HiddenMessages component
class HiddenMessages extends Component {

  constructor(props) {
    super(props)
    this.state = {
      message: [
        "The crow crows after midnight",
        "Jericho Jericho Go",
        "엄마가 섬그늘에 굴 따러 가면"
      ],
      showing: -1
    }
  }

  componentWillMount() {
    this.interval = setInterval(() => {
      this.setState(prevState => {
        let { showing, messages } = prevState
        showing = (++showing >= messages.length) ?
          0 : showing
        return {showing}
      })
    }, 1000)
  }

  componentWillMount() {
    clearInterval(this.interval)
  }

  render() {
    const { messages, showing } = this.state
    return (
      <div className="hiddden-messages">
        {messages.map((message, i) =>
          <HiddenMessage key={i} hide={(i!==showing)}>{message}</HiddenMessage>
        )}
      </div>
    )
  }
}
// HeddenMessage component
const Letter = XRegExp('\\pL','g')  // 유니코드 문자 클래스(\pL), global 옵션

class HiddenMessage extends Component {

  constructor(props) {
    super(props)
    this.state = {
      hidden: typeof props.hide === "boolean" ? props.hide : true
    }
  }

  render() {
    const { children } = this.props
    const { hidden } = this.state
    return (
      <p>
        {(hidden) ?
          children.replace(Letter, "x") :
          children
        }
      </p>
    )
  }
}
  • HiddenMessage component는 처음 마운트될 때 hide 프로퍼티를 사용해 상태를 결정하고 렌더링 시 hide가 ture면 메시지의 모든 글자를 x로 바꾼다. 하지만 부모가 hide 프로퍼티를 갱신하더라도 HiddenMessage component는 프로퍼티가 바뀐 사실을 인지하지 못하므로 아무 일도 벌어지지 않는다.
  • 이때 componentWillReceiveProps는 component의 프로퍼티를 부모가 변경하면 호출되는 method로 이 method 안에서 변경된 프로퍼티를 사용해 component 내부 상태를 바꿀 수 있다.
class HiddenMessage extends Component {

  constructor(props) {
    super(props)
    this.state = {
      hidden: typeof props.hide === "boolean" ? props.hide : true
    }
  }

  // 부모의 프로퍼티가 바뀐 경우 변경된 프로퍼티를 component 내부 상태에 반영한다.
  componentWillReceiveProps(nextProps) {
    this.setState({hidden: nextProps.hide})
  }
  
  render() {
    const { children } = this.props
    const { hidden } = this.state
    return (
      <p>
        {(hidden) ?
          children.replace(Letter, "x") :
          children
        }
      </p>
    )
  }
}
  • 위 코드 예제는 componentWillReceiveProps의 동작을 보여주기 위한 예제일 뿐 실제로는 스스로 내부의 상태를 바꾸는 것이 아니고 hide 프로퍼티에 따라 메시지를 다르게 표시하는 역할을 할 뿐이므로 부모 component가 상태를 관리하고 HiddenMessage는 상태가 없는 함수형 component를 사용해야 한다. 
  • 만약 component 내부에서 setState 호출이 필요하다면 그때는 상태를 도입하고, 그에 따라 componentWillReceiveProps method를 사용해 상태를 바꾸는 것은 정당하다.

3. React.Children

  • React.Children은 특정 component의 자식들을 다룰 수 있는 방법을 제공한다. 자식 노드들을 배열로 변환하거나, map을 적용하거나, 자식을 iteration하거나, 자식 수를 셀 수 있다. React.Children.only를 사용하면 오직 한 자식만 표시하는지 검사할 수도 있다.
// React.Children 예제
import { Children, PropTypes } from 'react'
import { render } from 'react-dom'

const Display = ({ ifTruthy=true, children }) =>
  (ifTruthy) ? Children.only(children) : null
  
const age = 22

render(
  <Display ifTruthy={age >= 21}>
    <h1>들어오세요</h1>
  </Display>,
  document.getElementById('react-container')
)
  • 위 예제에서 Display component는 h1 element 하나만 자식으로 표시한다. 만약 자식이 여럿 있었다면 리액트는 오류를 발생시킨다.(리액트 16에서는 발생하지 않는다.)
const findChild = (children, child) =>
  Children.toArray(children)
          .filter(c => c.type === child)[0]

const WhenTruthy = ({children}) => Children.only(children)
const WhenFalsy = ({children}) => Children.only(children)

const Display = ({ ifTruthy=True, children }) =>
  (ifTruthy) ?
    findChild(children, WhenTruthy) : findChild(children, WhenFalsy)

const age = 19

render(
  <Display ifTruthy={age >= 21}>
    <WhenTruthy>
      <h1>들어오세요</h1>
    </WhenTruthy>
    <WhenFalsy>
      <h1>애들은 가!</h1>
    </WhenFalsy>
  </Display>,
  document.getElementById('react-container')
)
  • Display component는 조건이 참인 경우 한 자식을 표시하고 거짓인 경우엔 다른 자식을 표시한다.
  • findChild 함수는 React.Children을 사용해 자식 component들을 배열로 만들고 이 배열에 filter 연산으로 component type을 사용해 자식을 찾는다.

출처 : learning react(2018)

728x90