리액트
#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