리액트

#6. Component 개선하기(2) - javascript 라이브러리 통합, 고차 Component

YJH3968 2021. 3. 31. 13:07
728x90

1. Fetch를 사용해 요청하기

  • Fetch는 WHATWG 그룹에서 만든 폴리필로 promise를 사용해 API 호출을 쉽게 해준다. 여기서는 리액트와 함께 잘 작동하는 isomorphic-fetch를 다룬다.
// isomorphic-fetch 설치
npm install isomorphic-fetch --save
  • component 생애주기 함수는 여러 javascript를 통합할 수 있는 지점을 제공한다.
  • API를 호출할 경우, component는 요청을 보내고 응답이 올 때까지의 대기 시간을 처리할 수 있어야 한다. 이러한 시간 지연 문제는 응답의 상태를 도입함으로써 해결할 수 있다. 
import { Component } from 'react'
import { render } from 'react-dom'
import fetch from 'isomorphic-fetch'

class CountryList extends Component {

  constructor(props) {
    super(props)
    this.state = {
      countryNames: [],
      loading: false
    }
  }

  componentDidMount() {
    this.setState({loading:true})
    fetch('https://restcountries.eu/rest/v1/all')
      .then(response => response.json())
      .then(json => json.map(country => country.name))
      .then(countryNames =>
        this.setState({countryNames, loading: false})
      )
  }

  render() {
    const { countryNames, loading } = this.state
    return (loading) ?
      <div>나라 이름 로딩 중...</div> :
      (!countryNames.length) ?
      <div>나라 이름이 없습니다.</div> :
        <ul>
          {countryNames.map(
            (x,i) => <li key={i}>{x}</li>
          )}
        </ul>
  }
}

render(
  <CountryList />,
  document.getElementById('react-container')
)

2 .D3 타임라인 포함시키기

  • 데이터 주도 문서(Data Driven Documents, D3)는 브라우저에서 데이터를 시각화해주는 javascript framework다.
  • D3는 데이터를 축소/확장하고 interpolate할 수 있는 다양한 도구를 제공한다.
  • 타임라인은 데이터 시각화 예제로 이벤트 날짜를 데이터로 받아서 시각적으로 각 이벤트의 시간 흐름을 보여준다. 역사적으로 더 과거에 일어난 이벤트일수록 왼쪽에 배치하고, 각 이벤트 사이의 시각적 간격은 시간 간격을 나타낸다.
  • 날짜 값을 해당 x좌표(픽셀) 값으로 변환하는 과정을 interpolate이라고 한다.
// D3 설치
npm install d3 --save
// ski event 데이터
const historicDatesForSkiing = [
  {
    year: 1879,
    event: "스키 생산 시작"
  },
  {
    year: 1882,
    event: "미국 스키 클럽 창설"
  },
  {
    year: 1924,
    event: "제 1회 겨울 올림픽 개최"
  },
  {
    year: 1926,
    event: "첫 번째 미국 스키샵이 문을 엶"
  },
  {
    year: 1932,
    event: "북아메리카 최초로 견인 로프 작동 시작"
  },
  {
    year: 1936,
    event: "첫 번째 리프트 작동 시작"
  },
  {
    year: 1949,
    event: "스쿼밸리, 매드 리버 글렌 개장"
  },
  {
    year: 1958,
    event: "첫 번째 곤돌라 작동 시작"
  },
  {
    year: 1964,
    event: "플라스틱 버클 부츠 출시"
  }
]
// ski event 데이
const historicDatesForSkiing = [
  {
    year: 1879,
    event: "스키 생산 시작"
  },
  {
    year: 1882,
    event: "미국 스키 클럽 창설"
  },
  {
    year: 1924,
    event: "제 1회 겨울 올림픽 개최"
  },
  {
    year: 1926,
    event: "첫 번째 미국 스키샵이 문을 엶"
  },
  {
    year: 1932,
    event: "북아메리카 최초로 견인 로프 작동 시작"
  },
  {
    year: 1936,
    event: "첫 번째 리프트 작동 시작"
  },
  {
    year: 1949,
    event: "스쿼밸리, 매드 리버 글렌 개장"
  },
  {
    year: 1958,
    event: "첫 번째 곤돌라 작동 시작"
  },
  {
    year: 1964,
    event: "플라스틱 버클 부츠 출시"
  }
]

// D3를 통한 시각화
import d3 from 'd3'
import { Component } from 'react'
import { render } from 'react-dom'

class Timeline extends Component {

  constructor({data=[]}) {
    // extent 함수는 수로 이루어진 배열에서 최솟값과 최댓값을 찾아준다.
    const times = d3.extent(data.map(d => d.year))
    const range = [10, 500]
    super({data})
    this.state = {data, times, range}
  }

  componentDidMount() {
    let group
    const { data, times, range } = this.state
    const { target } = this.refs
    // domain은 정의역을, range는 치역을 정한다.
    // scale 함수는 배율 함수로 시간의 값을 x축 상의 좌포로 interpolate해준다.
    const scale = d3.time.scale().domain(times).range(range)

    // D3로 SVG element를 만들고 그 element를 target element에 추가한다.
    d3.select(target)
      .append('svg')
      .attr('height', 230)
      .attr('width', 700)

    group = d3.select(target.children[0])
              .selectAll('g')
              .enter()
              .append('g')
              .attr(
                'transform',
                (d, i => 'translate(' + scale(d.year) + ', 0)')
              )

    group.append('circle')
        .attr('cy', 190)
        .attr('r', 5)
        .style('fill', 'blue')

    group.append('text')
        .text(d => d.year + ' - ' + d.event)
        .style('font-size', '9px')
        .attr('y', 130)
        .attr('x', -130)
        attr('transform', 'rotate(-45)')
  }



  render() {
    return (
      <div className="timeline">
        <h1>{this.props.name} 타임라인</h1>
        <div ref="target"></div>
      </div>
    )
  }
}



render(
  <Timeline name="스키의 역사" data={historicDatesForSkiing} />,
  document.getElementById('react-container')
)
  • D3로 SVG element를 만들고 그 element를 target element 참조에 추가하는 코드에서 UI를 만드는 작업은 리액트가 담당하므로 D3를 사용하는 대신 SVG component를 반환하는 Canvas component를 만든다.
// D3로 SVG element를 만드는 대신 SVG element를 반환하는 Canvas component를 만든다.
const Canvas = ({children}) =>
  <svg height="200" with="500">
    {children}
  </svg>
  • group element도 역시 DOM element이기 때문에 리액트가 처리해야 한다.
// group element를 설정하고 x축 상에 위치를 정해주기 위한 TimelineDot component
const TimelineDot = ({position, txt}) =>
  <g transform={`translate(${position},0)`}>
    <circle cy={160} r={5} style={{fill: 'blue'}} />
    <text y={115} x={-95} transform="rotate(-45)" style={{fontSize: '10px'}}>{txt}</text>
  </g>
  • 이제는 리액트가 가상 DOM을 활용해 UI를 관리한다. D3의 역할은 리액트가 하지 못하는 정의역과 치역의 계산, 연도를 좌표로 변경할 때 사용할 수 있는 scale 함수를 제공하는 것뿐이다. 이렇게 리팩토링한 Timeline component는 다음과 같다.
// 리팩토링한 Timeline

class Timeline extends Component {

  constructor({data=[]}) {
    const times = d3.extent(data.map(d => d.year))
    const range = [10, 500]
    super({data})
    this.scale = d3.time.scale().domain(times).range(range)
    this.state = {data, times, range}
  }
  render() {
    const { data } = this.state
    const { scale } = this
    return (
      <div className="timeline">
        <h1>{this.props.name} 타임라인</h1>
        <Canvas>
          {data.map((d, i) =>
            <TimelineDot position={scale(d.year)} txt={`${d.year} - ${d.event}`} />
          )}
        </Canvas>
      </div>
    )
  }
}
  • 이 챕터를 통해 알 수 있는 것은 리액트는 생애주기 함수를 통해 원하는 어떤 javascript 라이브러리와 통합해 작업할 수 있지만, UI의 경우 뷰를 처리하는 리액트의 역할이므로 외부 라이브러리를 되도록 쓰지 않는다는 것이다.

3. 고차 component(HOC)

  • 고차 component(HOC)는 리액트 component를 인자로 받아서 다른 리액트 component를 반환하는 함수다.
  • 보통 인자로 받은 component를 상태를 관리하는 component나 다른 기능을 부가하는 component로 감싸서 돌려준다.
  • 리액트 component 사이에서 기능을 재사용하는 가장 좋은 방법이다.
  • 합성된 결과 부모 component는 component에 프로퍼티를 전달할 수 있는 기능을 제공하거나 상태를 관리한다.
  • 합성된 component, 즉 자식 component는 부모 component가 제공하는 프로퍼티 이름이나 method 이름 외의 정보는 필요하지 않다. 그러므로 합성된 component는 HOC의 구현을 알 필요가 없다.  
// PeopleList 예제
import { Component } from 'react'
import { render } from 'react-dom'
import fetch from 'isomorphic-fetch'

class PeopleList extends Component {

  constructor(props) {
    super(props)
    this.state = [
      data: [],
      loaded: false,
      loading: false
    ]
  }

  componentWillMount() {
    this.setState({loading:true})
    fetch('https://randomuser.me/api/?result=10')
      .then(response => response.json)
      .then(obj => obj.results)
      .then(data => this.setState({
        loaded: true,
        loading: false,
        data
      }))
  }

  render() {
    const { data, loading, loaded } = this.state
    return (loading) ?
      <div>데이터 로딩 중...</div> :
      <ol className="people-list">
        {data.map((person, i) => {
          const {first, last} = person.name
          return <li key={i}>{first} {last}</li>
        })}
      </ol>
  }
}

render(
  <PeopleList />,
  document.getElementById('react-container')
)
  • 위 예제의 로딩 기능을 따로 떼어서 고차 Component로 만들면 component 전반에 같은 기능을 활용할 수 있다.
  • 이를 위해 PeopleList를 상태가 있는 부분에서 분리해서 상태가 없는 함수형 component로 독립시키고 데이터를 프로퍼티로부터 받게 한다.
// 상태가 없는 함수형 component로 분리한 PeopleList
const PeopleList = ({data}) =>
  <ol className="people-list">
    {data.results.map((person, i) => {
      const {first, last} = person.name
      return <li key={i}>{first} {last}</li>
    })}
  </ol>
  • 그리고 DataComponent라는 HOC를 추가해 데이터 처리, 데이터를 받는 동안의 UI 표시, 그리고 loading과 관련된 상태를 담당한다.
// HOC 예시
const DataComponent = (ComposedComponent, url) =>
  class DataComponent extends Component {
    constructor(props) {
      super(props)
      this.state = {
        data: [],
        loading: false,
        loaded: false
      }
    }

    componentWillMount() {
      this.setState({loading:true})
      fetch(url)
        .then(response => response.json())
        .then(data => this.setState({
          loaded:true,
          loading: false,
          data
        }))
    }

    render() {
      return (
        <div className="data-component">
          {(this.state.loading) ?
            <div>데이터 로딩 중...</div> :
            <ComposedComponent {...this.state} />}
        </div>
      )
    }
  }
  
// 이제 이를 적용하기 위해서 PeopleList와 url을 인자로 넣어 새로운 component를 만든다.
const RandomMeUsers = DataComponent(
  PeopleList,
  "https://randomuser.me/api/"
)

render(
  <RandomMeUsers count={10} />,
  document.getElementById('react-container')
)
  • 이를 이용하면 모든 유형의 데이터 component를 쉽게 만들 수 있다.
// HOC를 적용해볼 CountryDropDown Component
const countryNames = ({data, selected=""}) =>
  <select className="people-list" defaultValue={selected}>
    {data.map(({name}, i) =>
      <option key={i} value={name}>{name}</option>
    )}
  </select>

const CountryDropDown = 
  DataComponent(
    CountryNames,
    "https://restcountries.eu/rest/v1/all"
  )

render(
  <CountryDropDown selected="United States" />, // HOC에 프로퍼티를 전달한다.
  document.getElementById('react-container')
)
  • 위 예제의 경우 CountryDropDown을 HOC를 통해 만들고 여기에 프로퍼티를 전달한 경우다. 하지만 DataComponent는 상태만 합성된 component에 전달하기 때문에 이 프로퍼티는 전달되지 수 없다. 그래서 자식 component에 프로퍼티도 전달하도록 코드를 변경한다.
// DataComponent의 render()에서 합성된 component에 프로퍼티를 전달하도록 코드를 변경한다.
render() {
  return (
    <div className="data-component">
      {(this.state.loading) ?
        <div>데이터 로딩 중...</div> :
        <ComposedComponent {...this.state} {...this.props} />
      }
    </div>
  )
}
  • 다음 코드는 내용의 일부를 보여주거나 숨기는 기능을 추가해주는 HOC이다.
import { Component } from 'react'

const Expandable = ComposedComponent =>
  class Expandable extends Component {
    constructor(props) {
      super(props)
      const collapsed =
        (props.hidden && props.hidden === true) ? true : false
      this.state = {collapsed}
      this.expandCollapse = this.expandCollapse.bind(this)
    }

    expandCollapse() {
      let collapsed = !this.state.collapsed
      this.setState({collapsed})
    }

    render() {
      return <ComposedComponent expandCollapse={this.expandCollapse}
        {...this.state} {...this.props} />
    }
  }
  • collapsed라는 상태를 토글하는 method도 추가하고 이를 프로퍼티로 전달하면서 합성된 component는 이 method를 이용해 Expandable의 collapsed 상태를 바꾸고, 이로 인해 합성된 component가 새 상태에 맞춰 렌더링된다.
// Expandable을 통해 간단히 나타낸 HiddenMessage 예제
const ShowHideMessage = ({children, collapsed, expandCollapse}) =>
  <p onClick={expandCollapse}>
    {(collapsed) ? children.replace(Letter, "x") : children}
  </p>

const HiddenMessage = Expandable(ShowHideMessage)
// MenuBtton을 사용해 내용 표시 여부를 토글할 수 있는 PopUpButton 예제
class MenuButton extends Component {
  componentWillReceiveProps(nextProps) {
    const collapsed =
      (nextProps.collapsed && nextProps.collapsed === true) ? true : false
    this.setState({collapsed})
  }

  render() {
    const {children, collapsed, txt, expandCollapse} = this.props
    return (
      <div className="pop-button">
        <button onClick={expandCollapse}>{txt}</button>
        {(!collapsed) ?
          <div className="pop-up">
            {children}
          </div> :
          ""
        }
      </div>
    )
  }
}

const PopUpButton = Expandable(MenuButton)

render(
  <PopUpButton hidden={true} txt="팝업 토글">
    <h1>숨겨진 content</h1>
    <p>이 content는 처음에 숨겨져 있습니다.</p>
  </PopUpButton>,
  document.getElementById('react-container')
)

 

 

출처 : learning react(2018)

728x90