[React] 공통기능분리(2) - 렌더속성값 (렌더속성값 패턴으로 githubAPI 이용하기)

권준혁·2020년 11월 1일
0

React

목록 보기
16/20
post-thumbnail

안녕하세요!
공통기능을 관리하는 방법 두 번째 Ref속성값에 대한 포스팅입니다.
Ref속성값은 코드 재사용을 위해 함수 타입의 속성값을 이용하는 패턴을 말합니다.

기본형태

기본형태에 대해서 알아보기전에 리액트 요소를 다시 한 번 살펴볼 필요가 있었습니다.

리액트요소

{
    type : 'a',
    key : 'key1',
    ref : 'null',
    props : {
      href : 'http://google.com',
      style : {
        width:100,
      },
      children : 'click here'
    },
}

대략 이런 형태였습니다.


다음은 코드를 작성하겠습니다.
여기서는 render메서드가 조금 특이한데 컴포넌트를 작성하는게 아닌, 부모컴포넌트 MyComponent에서 children속성에 작성한 함수를 호출합니다.

일반적인 방법에서는 자식컴포넌트에서 렌더링에대해 작성이 돼있는 상태에서 부모컴포넌트에서 속성값만을 입력하는 형태였는데, 렌더속성값을 이용하는 패턴에서는 부모컴포넌트에서 자식컴포넌트의 렌더링내용까지 작성합니다.
여기서는 children속성을 이용했지만 다른부분에서도 렌더속성을 정의할 수도 있습니다.

MountEvent컴포넌트는 공통기능을 분리하기 위한 컴포넌트입니다.
단순히 마운트시에 sendMountEvent함수를 실행시키기 위한 목적임을 잊으면 헷갈립니다.

// MountEvent.js
import React from 'react'
export default class MountEvent extends React.Component {
    componentDidMount () {
        const {name} = this.props;
        sendMountEvent(name)
    }
    render () {
        const {children} = this.props;
        return children();
    }
}
function sendMountEvent (name) {
    // ...
    console.log(name)
}

MyComponent는 MountEvent컴포넌트를 이용해 MountEvent가 가진 기능(마운트 시 sendMountEvent()함수 실행)을 사용해보기 위해 작성합니다.

// MyComponent.js
import React from 'react'
import MountEvent from "./MountEvent";
export default class MyComponent extends React.Component {
    // ...
    render () {
        return (
        <MountEvent name='MyComponent'>
          {()=><div><p>children</p></div>}
        </MountEvent>
        )
    }
}

앞서 MountEvent컴포넌트에서 children속성 부분을 실행해 렌더링하도록 작성했었습니다.
따라서 MyComponent에서 MountEvent를 작성 할 때 children부분을 함수로 작성해줍니다.
그래서 MountEvent가 마운트 되고 나면 componentDidMount에서 name속성을 인수로 이용해 sendMountEvent로직을 수행하고,
다시 한 번, props.children으로 참조하고있는 함수를 실행시켜 렌더링을 수행합니다.


렌더속성값으로 데이터처리 로직 구현하기 (githubAPI)

[서버로 데이터를 요청하고 데이터를 처리하는 로직]렌더 속성값으로 구현한 코드입니다.

// DataFetcher.js
import React from 'react'
import axios from 'axios'
export default class DataFetcher extends React.Component {
    state = {
        data : null
    }
    componentDidMount () {
        const {url,parseData} = this.props;
        axios(url).then(response=>{
            const data = parseData(response.data);
            this.setState({data})
        });
    }
    render () {
        const {children} = this.props;
        const {data} = this.state;
        if (data=== null) {
            return <p>data loading</p>
        }else {
            return children(data)
        }
    }
}

componentDidMount메서드에서 props에 작성된 url과 parseData함수를 이용합니다.
axios(비동기통신 라이브러리)를 이용해 props에 전달된 url에 data를 요청합니다.
data를 받아서 props에 전달된 parseData함수를 이용해 데이터를 가공합니다.
가공된 데이터를 state에 저장합니다.
state에 저장된 데이터가 없다면 "data loading"을 출력하고, 아니라면 children에 작성된 함수에 data를 인수로 호출합니다.
마치 추상적으로 작성된 함수 같습니다.

다음은 DataFetcher컴포넌트를 렌더링할 MyComponent컴포넌트입니다.

// MyComponent.js
import React from 'react'
// import MountEvent from "./MountEvent";
import DataFetcher from './DataFetcher'
export default class MyComponent extends React.Component {
    render () {
        return (
            <DataFetcher
             url='https://api.github.com/repos/facebook/react'
             parseData={parseRepoData}
             >
                 {(data)=>{
                     <div>
                         <p>{`name : ${data.name}`}</p>
                         <p>{`stars : ${data.stars}`}</p>
                         <p>{`open issues : ${data.openIssues}`}</p>
                     </div>
                 }}
            </DataFetcher>
        )
    }
}
function parseRepoData(data) {
    return {
        name : data.name,
        stars: data.stargazers_count,
        openIssues:data.open_issues
    }
}
  • 아까 작성한 DataFetcher컴포넌트에서 기능을 수행하기 위해 필요한 속성이 뭐가 있었는지 생각해보고, 빠짐없이 props에 넣어줘야겠습니다.
  • url과 parseData함수, children이 필요했었습니다.
  • parseData함수는 인스턴스에 접근이 필요없기 때문에 외부함수로 작성합니다.
  • chlildren은 DataFetcher가 마운트되고 비동기통신이 완료되어 data인수와 함께 호출되기 때문에 data를 이용해 직접 보여질 view를 작성합니다.

    잘 출력되는 모습입니다!

또 다른 예제입니다.

마우스 위치추적

마우스 위치정보를 출력하는 예제입니다.

// MouseTracer.js
import React from 'react'
export default class MouseTracer extends React.Component {
    state = {
        x : null,
        y : null,
    }
    onMouseMove = e => {
        this.setState({
            x : e.clientX,
            y : e.clientY
        })
    }
    render () {
        const {children} = this.props;
        const {x,y} = this.state;
    return <div onMouseMove={this.onMouseMove}>{children({x,y})}</div>
    }
}
// MyComponent.js
import React from 'react'
import MouseTracer from "./MouseTracer";
export default class MyComponent extends React.Component {
    render () {
        return (
        <MouseTracer>{({x,y})=><p>{`( x , y ) : ( ${x} , ${y} )`}</p>}</MouseTracer>
        )
    }
}

실행화면


렌더속성값 함수의 매개변수를 속성값으로 전달하는 패턴

// MountInfo.js
import React from 'react'
export default class extends React.Component {
    state = {
        hasMounted : false
    }
    componentDidMount () {
        this.setState({hasMounted : true})
    }
    render () {
        const {children} = this.props;
        const {hasMounted} = this.state;
        return children({hasMounted})
    }
}
// MyComponent.js
import React from 'react'
import MountInfo from "./MountInfo";
class MyComponent extends React.Component {
    componentDidUpdate () {
        const {hasMounted} = this.props;
        console.log(`lifecycle functions can access hasMounted(${hasMounted})`)
    }
    render () {
        const {hasMounted} = this.props;
        return <p>{`hasMounted : ${hasMounted}`}</p>
    }
}
export default class WrapperComponent extends React.Component {
    render () {
        const props = this.props
        return (
            <MountInfo>
                {({hasMounted}) => (
                    <MyComponent {...props} hasMounted={hasMounted}/>
                )}
            </MountInfo>
        )
    }
}

속성값으로 전달해 줄 컴포넌트가 필요하기 때문에, export할 때, MyComponen의 WrapperComponent를 만들어 내보내줍니다. WrapperComponent가 render메서드에서 MountInfo를 호출해 child속성 함수가 MyComponent를 리턴하도록 합니다.
제 3의 컴포넌트가 있었다면 WrapperComponent가 필요 없었겠지만, WrapperComponent를 사용하면 바로 사용할 수 있습니다.
WrapperComponent를 함수형으로 바꾸면 조금은 간결해집니다.

// export 부분 수정
export default function WrapperComponent (props) {
    return (
        <MountInfo>
            {({hasMounted}) => (
                <MyComponent {...props} hasMounted={hasMounted}/>
            )}
        </MountInfo>
    )
}

정리하면, 렌더 속성값 함수의 매개변수를 속성값으로 전달하려면 "WrapperComponent를 만들어 속성값을 render메서드에서 전달하도록 만든다." 입니다.

여기까지 렌더속성값으로 공통기능관리하는 방법에 대해 알아봤습니다.
렌더 속성값은 콜백지옥처럼 코드가 어수선해지는 경향이 있긴 하지만, 공통기능을 관리하는데에 좋은 선택지라고 합니다.

공통기능을 관리하기위한 패턴

  • 고차컴포넌트
  • 렌더속성값

두 가지를 이용해 좋은 앱을 만들어봐야겠습니다.

읽어주셔서 감사합니다.

profile
웹 프론트엔드, RN앱 개발자입니다.

0개의 댓글