Render Prop

우리가 일전에 HoC 를 통하여 반복되는 로직을 효율적으로 재사용 하는 방법을 알아봤었습니다. 이번 강좌에서 알아볼 render props 는, HoC 에서 처럼 반복되는 로직을 쉽게 재사용 할 수 있게 해주고, 컴포넌트 코드를 작성하는 과정에서 컴포넌트를 함수로 감싸는 것이 아니라, JSX 에서 렌더링 하는 방식으로 사용 할 수 있게 해주는 패턴입니다.

Render Prop 은, 단순히 props 에 JSX 를 렌더링하는 함수를 전달 하는 것 입니다. 그리고 render 함수에서, 전달받은 함수에, 넣어주고 싶은 값을 함수의 인자로 전달하여 사용합니다.

import React, { Component } from 'react';

class Example extends Component {
  state = {
    value: 'hello!'
  };
  render() {
    return this.props.render(this.state.value);
  }
}

export default Example;

그리고 이 컴포넌트를 사용 할 때엔 이렇게 사용하죠:

import React, { Component } from 'react';
import Example from './Example';

class App extends Component {
  render() {
    return (
      <div>
        <Example render={value => <h1>{value}</h1>} />
      </div>
    );
  }
}

export default App;

그럼, 다음과 같이 나타나게 됩니다.

음? 뭐가 어떻게 된거죠?

한 30초 동안만 이 코드의 구조를 보면서 이해를 해보세요. 그냥 단순히, props 로 value 라는 값을 전달받는 render 라는 함수를 설정해 주었고, Example 에선 이 함수에 value 값을 넣어주기 위해서 컴포넌트의 render 함수에서 this.state.value 를 함수의 인자로 전달해주었죠.

자! 그럼 이걸 가지고 무엇을 할 수 있을까요?

Render Prop 예제 #1 폼 관리

일단 여기서 팩트 하나! 모든 HoC 는 Render Prop 으로도 구현이 가능합니다.

그래서 기존에 HoC 로 해결하던 것들은 Render Prop 으로도 해결 가능합니다.

한번, 다음 코드를 확인해보세요, 두개의 폼이 있는데, 각 컴포넌트에서 비슷한 코드가 사용됩니다.

FormOne.js

import React, { Component } from 'react';

class FormOne extends Component {
  state = {
    name: '',
    phone: ''
  };

  handleChange = e => {
    this.setState({
      [e.target.name]: e.target.value
    });
  };

  handleSubmit = e => {
    e.preventDefault();
    this.props.onSubmit(this.state);
    this.setState({
      name: '',
      phone: ''
    });
  };

  render() {
    const { name, phone } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          name="name"
          placeholder="이름"
          value={name}
          onChange={this.handleChange}
        />
        <input
          name="phone"
          placeholder="전화번호"
          value={phone}
          onChange={this.handleChange}
        />
        <button type="submit">OK</button>
      </form>
    );
  }
}

export default FormOne;

FormTwo.js

import React, { Component } from 'react';

class FormOne extends Component {
  state = {
    name: '',
    phone: ''
  };

  handleChange = e => {
    this.setState({
      [e.target.name]: e.target.value
    });
  };

  handleSubmit = e => {
    e.preventDefault();
    this.props.onSubmit(this.state);
    this.setState({
      name: '',
      phone: ''
    });
  };

  render() {
    const { name, phone } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          name="name"
          placeholder="이름"
          value={name}
          onChange={this.handleChange}
        />
        <input
          name="phone"
          placeholder="전화번호"
          value={phone}
          onChange={this.handleChange}
        />
        <button type="submit">OK</button>
      </form>
    );
  }
}

export default FormOne;

폼을 만들때마다 이런 비슷한 코드를 자꾸 작성하게 되는데요... 이걸 Render Prop 으로 해결해봅시다.

FormManager.js

import React, { Component } from 'react';

class FormManager extends Component {
  static defaultProps = {
    form: {}
  };
  constructor(props) {
    super(props);
    this.state = props.defaultForm;
  }
  handleChange = e => {
    this.setState({
      [e.target.name]: e.target.value
    });
  };
  handleSubmit = e => {
    e.preventDefault();
    this.props.onSubmit(this.state);
    this.setState(this.props.form);
  };
  render() {
    return this.props.render({
      form: this.state,
      onSubmit: this.handleSubmit,
      onChange: this.handleChange
    });
  }
}

export default FormManager;

사용 할 땐 이렇게 사용하면 됩니다.

App.js

import React, { Component } from 'react';
import FormOne from './FormOne';
import FormTwo from './FormTwo';
import FormManager from './FormManager';

class App extends Component {
  handleSubmitFormOne = form => {
    console.log('Submit Form One', form);
  };
  handleSubmitFormTwo = form => {
    console.log('Submit Form Two', form);
  };
  render() {
    return (
      <div>
        <FormManager
          defaultForm={{ name: '', phone: '' }}
          onSubmit={this.handleSubmitFormOne}
          render={({ form, onSubmit, onChange }) => (
            <FormOne
              name={form.name}
              phone={form.phone}
              onSubmit={onSubmit}
              onChange={onChange}
            />
          )}
        />
        <FormManager
          defaultForm={{ username: '', password: '' }}
          onSubmit={this.handleSubmitFormTwo}
          render={({ form, onSubmit, onChange }) => (
            <FormTwo
              username={form.username}
              password={form.password}
              onSubmit={onSubmit}
              onChange={onChange}
            />
          )}
        />
      </div>
    );
  }
}

export default App;

조금씩 이해가 가기 시작했나요?

Render Prop 예제 #2 API 호출

예제를 한가지 더 다뤄보면서 이 패턴에 조금 더 익숙해져봅시다!

아래 프로젝트에서 작업하겠습니다:

이전에 우리가 withRequest 라는 HoC 를 만들어서 반복되는 코드를 재사용 할 수 있도록 해줬었죠? 이번에는 render prop 으로 이걸 해결해봅시다!

DataFetcher.js

import React, { Component } from "react";
import axios from "axios";

class DataFetcher extends Component {
  state = {
    loading: false,
    data: null,
    error: null
  };

  async initialize() {
    this.setState({
      loading: true
    });
    try {
      const response = await axios.get(this.props.url);
      this.setState({
        loading: false,
        data: response.data
      });
    } catch (e) {
      this.setState({
        loading: false,
        error: e
      });
    }
  }

  componentDidMount() {
    this.initialize();
  }

  render() {
    if (this.state.error) {
      return <div>에러발생!</div>
    }
    if (this.state.loading || !this.state.data) {
      return <div>로딩중..</div>;
    }
    return this.props.render(this.state.data);
  }
}
export default DataFetcher;

요청 관련 작업을 하고, 로딩중일땐 "로딩중" 이란 텍스트가 뜨게 했고, 에러시엔 "에러 발생!" 이 뜨게 했습니다. 요청이 정상적으로 이뤄졌다면, props 로 받은 render 에 현재 데이터를 넣어서 전달해줍니다.

그리고, Posts 와 Comments 컴포넌트는 다음과 같이 간소화 시키세요.

Post.js

import React from "react";

const Post = ({ title, body }) => (
  <div>
    <h2>{title}</h2>
    <p>{body}</p>
  </div>
);

export default Post;

Comments.js

import React from "react";

const Comments = ({ comments }) => (
  <ul>{comments.map(comment => <li key={comment.id}>{comment.body}</li>)}</ul>
);

export default Comments;

그리고, DataFetcher 를 통해서 다음과 같이 요청을 처리하시면 됩니다!

App.js

import React, { Component } from "react";
import DataFetcher from "./DataFetcher";
import Post from "./Post";
import Comments from "./Comments";

class App extends Component {
  render() {
    return (
      <div>
        <h1>포스트</h1>
        <DataFetcher
          url="https://jsonplaceholder.typicode.com/posts/1"
          render={data => <Post title={data.title} body={data.body} />}
        />
        <hr />
        <h1>덧글</h1>
        <DataFetcher
          url="https://jsonplaceholder.typicode.com/comments?postId=1"
          render={data => <Comments comments={data} />}
        />
      </div>
    );
  }
}

export default App;

참 쉽죠?

children 으로 render prop

우리가 작성한 코드에서는, this.props.render 를 통하여 render prop 을 구현해주었는데요, children 을 사용하셔도 무방합니다. 하지만 americanexpress 에 있는 포스트에서는 이를 그닥 추천하진 않습니다. 저 또한 해당 포스트를 읽어보니 children 으로 함수를 전달하는건 그렇게 좋은것 같지는 않습니다. 물론, 리액트 v16.3 에 도입된 Context Consumer 는 chlidren 을 사용하긴 하지만요.

위 포스트에서의 주요 요점은, "우리가 변수명을 지을 때, 의미 있는 이름을 사용해야 하는데 children 를 함수로 사용하는것은 전혀 의미있지 않다" 입니다.

HoC 와 Render Prop 의 주요 차이

Render Prop 을 사용함으로서 최고 이점은 컴포넌트가 가지고 있는 기능 - LifeCycle API, JSX 등을 온전히 누릴 수 있습니다. HoC 는, 적용하려면 컴포넌트를 또 따로 만들어야 하는 반면, Render Prop 은 이미 만들어진 컴포넌트를 가지고 JSX 단에서 기능을 붙여줄 수 있습니다. 추가적으로, 원래는 HoC 의 파라미터로 넣어줘야 할 설정들을 동적으로 처리해줄 수도 있죠.

추가적으로 HoC 는 props 이름이 충돌 될 수있는데 render prop 은 그럴 일이 없죠. 이런 식으로 사용하면 되니까요;

<RenderProp
 render={foo => <RenderPopTwo 
    render={bar => <div><{foo} {bar}</div>
  />}
}/>

상황에 따라 HoC 가 간결 할 수도 있습니다. 예를들어서, 리덕스의 connect 함수는 HoC 로 사용하는 편이 조금 더 보기 쉽겠죠?

Render Prop 패턴을 잘 알아두시면, 나중에 유용하게 써먹을 수 있으실 겁니다! :)

Reference

results matching ""

    No results matching ""