Skip to content
This repository has been archived by the owner on Apr 13, 2020. It is now read-only.

Latest commit

ย 

History

History
994 lines (825 loc) ยท 30.9 KB

File metadata and controls

994 lines (825 loc) ยท 30.9 KB

5. ์‹ฌํ™”: ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ

์ด๋ฒˆ ์žฅ์—์„œ๋Š” ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ ์‹ฌํ™” ๋‚ด์šฉ์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ(higher order components)๋ฅผ ํ•™์Šตํ•œ ํ›„ ์‹ค์ œ ๊ตฌํ˜„์„ ํ•ด๋ณผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ ๊ณ ๊ธ‰ ์ฃผ์ œ์™€ ๋ณต์žกํ•œ ์ธํ„ฐ๋ ‰์…˜๋„ ํ•จ๊ป˜ ์•Œ์•„๋ด…์‹œ๋‹ค.

5.1 Ref ยท DOM

์ข…์ข… ๋ฆฌ์•กํŠธ์—์„œ DOM ๋…ธ๋“œ๋ฅผ ๋‹ค๋ฃฐ ์ผ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ref ์†์„ฑ์œผ๋กœ DOM ๋…ธ๋“œ๋ฅผ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ์˜ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„๊ณผ ์„ ์–ธ ๋ฐฉ์‹์— ์œ„๋ฐฐ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์•ˆํ‹ฐ ํŒจํ„ด(anti pattern)์ด๋ผ๊ณ ๋„ ํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์ž…๋ ฅ ํ•„๋“œ๋ฅผ ๊ตฌํ˜„ํ–ˆ์„ ๋•Œ, ์ž ์‹œ ์–ธ๊ธ‰ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ ๊ณต์‹ ๋ฌธ์„œ์—๋Š” ref๊ฐ€ ํ•„์š”ํ•œ ์„ธ ๊ฐ€์ง€ ๊ฒฝ์šฐ๋ฅผ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

  • DOM API (focus, media playback ๋“ฑ)์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ
  • ๋ช…๋ นํ˜• ๋…ธ๋“œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ํ˜ธ์ถœํ•˜ํ•˜๋Š” ๊ฒฝ์šฐ
  • DOM ๋…ธ๋“œ๊ฐ€ ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(์˜ˆ: D3.js)๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ

Search ์ปดํฌ๋„ŒํŠธ๋ฅผ ์˜ˆ์ œ๋กœ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ฒ˜์Œ ๋ Œ๋”๋ง ๋  ๋•Œ ์ž…๋ ฅ ํ•„๋“œ(input field)๋ฅผ ํฌ์ปค์Šค(focus) ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” DOM API๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์„ค๋ช…ํ•˜๊ฒ ์ง€๋งŒ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค๊ณ  ์žˆ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—๋Š” ์‹ค์งˆ์ ์ธ ๋„์›€์€ ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ดํ›„ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์€ ์ƒ๋žตํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ํ•จ์ˆ˜ํ˜• ๋น„ ์ƒํƒœ ์ปดํฌ๋„ŒํŠธ์™€ ES6 ํด๋ž˜์Šค ์ปดํฌ๋„ŒํŠธ ๋ชจ๋‘ ref ์†์„ฑ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํฌ์ปค์Šค ์‚ฌ์šฉ์„ ์œ„ํ•ด ์ƒ๋ช…์ฃผ๊ธฐ ๋ฉ”์„œ๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ES6 ํด๋ž˜์Šค ์ปดํฌ๋„ŒํŠธ์™€ ํ•จ๊ป˜ ref ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ input ํ•„๋“œ์— ํฌ์ปค์Šค(focus) ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค.

๋จผ์ € ํ•จ์ˆ˜ํ˜• ๋น„ ์ƒํƒœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ES6 ํด๋ž˜์Šค ์ปดํฌ๋„ŒํŠธ๋กœ ๋ฆฌํŒฉํ„ฐ๋ง ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

# leanpub-start-insert
class Search extends Component {
  render() {
    const {
      value,
      onChange,
      onSubmit,
      children
    } = this.props;

    return (
# leanpub-end-insert
      <form onSubmit={onSubmit}>
        <input
          type="text"
          value={value}
          onChange={onChange}
        />
        <button type="submit">
          {children}
        </button>
      </form>
# leanpub-start-insert
    );
  }
}
# leanpub-end-insert

ES6 ํด๋ž˜์Šค ์ปดํฌ๋„ŒํŠธ์˜ this ๊ฐ์ฒด๋Š” ref ์†์„ฑ์œผ๋กœ DOM ๋…ธ๋“œ๋ฅผ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

class Search extends Component {
  render() {
    const {
      value,
      onChange,
      onSubmit,
      children
    } = this.props;

    return (
      <form onSubmit={onSubmit}>
        <input
          type="text"
          value={value}
          onChange={onChange}
# leanpub-start-insert
          ref={(node) => { this.input = node; }}
# leanpub-end-insert
        />
        <button type="submit">
          {children}
        </button>
      </form>
    );
  }
}

์ด์ œ this ๊ฐ์ฒด, ์ƒ๋ช…์ฃผ๊ธฐ ๋ฉ”์†Œ๋“œ์™€ DOM API๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งˆ์šดํŠธ ๋์„ ๋•Œ input ํ•„๋“œ์— ํฌ์ปค์Šค๋ฅผ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

class Search extends Component {
# leanpub-start-insert
  componentDidMount() {
    this.input.focus();
  }
# leanpub-end-insert

  render() {
    const {
      value,
      onChange,
      onSubmit,
      children
    } = this.props;

    return (
      <form onSubmit={onSubmit}>
        <input
          type="text"
          value={value}
          onChange={onChange}
          ref={(node) => { this.input = node; }}
        />
        <button type="submit">
          {children}
        </button>
      </form>
    );
  }
}

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋ Œ๋”๋ง๋  ๋•Œ input ํ•„๋“œ์— ํฌ์ปค์Šค๋ฅผ ์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ๊ธฐ๋ณธ์ ์œผ๋กœ ref ์†์„ฑ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ this ๊ฐ์ฒด๊ฐ€ ์—†๋Š” ํ•จ์ˆ˜ํ˜• ๋น„ ์ƒํƒœ ์ปดํฌ๋„ŒํŠธ๋Š” ref๋ฅผ ์–ด๋–ป๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”? ์•„๋ž˜ ํ•จ์ˆ˜ํ˜• ๋น„ ์ƒํƒœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

const Search = ({
  value,
  onChange,
  onSubmit,
  children
}) => {
# leanpub-start-insert
  let input;
# leanpub-end-insert
  return (
    <form onSubmit={onSubmit}>
      <input
        type="text"
        value={value}
        onChange={onChange}
# leanpub-start-insert
        ref={(node) => input = node}
# leanpub-end-insert
      />
      <button type="submit">
        {children}
      </button>
    </form>
  );
}

์ด์ œ DOM ์—˜๋ ˆ๋จผํŠธ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํฌ์ปค์Šค ์˜ˆ์ œ์˜ ๊ฒฝ์šฐ ๋ณ„๋‹ค๋ฅธ ๋„์›€์ด ๋˜์ง€ ์•Š์„ ๊ฒ๋‹ˆ๋‹ค. ํ•จ์ˆ˜ํ˜• ๋น„ ์ƒํƒœ ์ปดํฌ๋„ŒํŠธ์—์„œ ํฌ์ปค์Šค๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” ์ƒ๋ช…์ฃผ๊ธฐ ๋ฉ”์„œ๋“œ๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ฐ€๊นŒ์šด ๋ฏธ๋ž˜์—๋Š” ref ์†์„ฑ์„ ๊ฐ€์ง„ ํ•จ์ˆ˜ํ˜• ๋น„ ์ƒํƒœ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฐ€๋Šฅํ• ์ง€๋„ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค.

์ฝ์–ด๋ณด๊ธฐ

5.2 Loading ์ปดํฌ๋„ŒํŠธ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๋‹ค์‹œ ๋Œ์•„๊ฐ€ ๋ด…์‹œ๋‹ค. ํ•ด์ปค ๋‰ด์Šค API๋กœ ๊ฒ€์ƒ‰ ์š”์ฒญํ•˜๋Š” ๋™์•ˆ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์„ ๊ฒ๋‹ˆ๋‹ค. ๋น„๋™๊ธฐ ์š”์ฒญ์ž„์œผ๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ทธ๋™์•ˆ ๊ธฐ๋‹ค๋ฆด ๊ฒƒ์„ ์•Œ๋ ค์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค. src/App.js ํŒŒ์ผ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Loading ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

const Loading = () =>
  <div>Loading ...</div>

๋‹ค์Œ ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๋Š” ํ”„๋กœํผํ‹ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋กœ๋”ฉ ์ƒํƒœ์— ๋”ฐ๋ผ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ณด์—ฌ์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

class App extends Component {

  constructor(props) {
    super(props);

    this.state = {
      results: null,
      searchKey: '',
      searchTerm: DEFAULT_QUERY,
      error: null,
# leanpub-start-insert
      isLoading: false,
# leanpub-end-insert
    };

    ...
  }

  ...

}

isLoading ํ”„๋กœํผํ‹ฐ์˜ ์ดˆ๊ธฐ ๊ฐ’์€ false์ž…๋‹ˆ๋‹ค. App ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งˆ์šดํŠธ๋˜๊ธฐ ์ „์—๋Š” ๋กœ๋”ฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์š”์ฒญํ•  ๋•Œ ๋กœ๋”ฉ ์ƒํƒœ ๊ฐ’์„ true๋กœ ๋ฐ”๊พธ๊ณ , ์š”์ฒญ์ด ์„ฑ๊ณต๋˜๋ฉด ๋‹ค์‹œ false๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

class App extends Component {

  ...

  setSearchTopStories(result) {
    ...

    this.setState({
      results: {
        ...results,
        [searchKey]: { hits: updatedHits, page }
      },
# leanpub-start-insert
      isLoading: false
# leanpub-end-insert
    });
  }

  fetchSearchTopStories(searchTerm, page = 0) {
# leanpub-start-insert
    this.setState({ isLoading: true });
# leanpub-end-insert

    fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`)
      .then(response => response.json())
      .then(result => this.setSearchTopStories(result))
      .catch(e => this.setState({ error: e }));
  }

  ...

}

์ด์ œ App ์ปดํฌ๋„ŒํŠธ ์•ˆ์— Loading ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์œผ๋กœ ๋กœ๋”ฉ ์ƒํƒœ์— ๋”ฐ๋ผ Loading ์ปดํฌ๋„ŒํŠธ ๋˜๋Š” Button ์ปดํฌ๋„ŒํŠธ ๋‘˜ ์ค‘ ํ•˜๋‚˜๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. Button ์ปดํฌ๋„ŒํŠธ๋Š” ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฒ„ํŠผ์ž…๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

class App extends Component {

  ...

  render() {
    const {
      searchTerm,
      results,
      searchKey,
      error,
# leanpub-start-insert
      isLoading
# leanpub-end-insert
    } = this.state;

    ...

    return (
      <div className="page">
        ...
        <div className="interactions">
# leanpub-start-insert
          { isLoading
            ? <Loading />
            : <Button
              onClick={() => this.fetchSearchTopStories(searchKey, page + 1)}>
              More
            </Button>
          }
# leanpub-end-insert
        </div>
      </div>
    );
  }
}

์ดˆ๊ธฐ์— componentDidMount() ๋ฉ”์„œ๋“œ์—์„œ ์š”์ฒญํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹œ์ž‘ํ•  ๋•Œ Loading ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋น„์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์— Table ์ปดํฌ๋„ŒํŠธ๋Š” ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•ด์ปค ๋‰ด์Šค API์—์„œ ์‘๋‹ต์„ ๋ฐ›์œผ๋ฉด result๊ฐ€ ํ‘œ์‹œ๋˜๊ณ  isLoading ์ƒํƒœ๋Š” false๋กœ ์„ค์ •๋˜๋ฉฐ Loading ์ปดํฌ๋„ŒํŠธ๋Š” ์‚ฌ๋ผ์ง€๊ณ  "More" ๋ฒ„ํŠผ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๋ฒˆ ๋” ๊ฐ€์ ธ์˜ค๋ฉด, ์ด ๋ฒ„ํŠผ์ด ์‚ฌ๋ผ์ง€๊ณ  ๋‹ค์‹œ Loading ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ณด์ž…๋‹ˆ๋‹ค.

์‹ค์Šตํ•˜๊ธฐ

  • "Loading ..." ํ…์ŠคํŠธ ๋Œ€์‹  Font Awesome ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ๋กœ๋”ฉ ์•„์ด์ฝ˜์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

5.3 ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ(Higher order components, ์ดํ•˜ HOCs)๋Š” ๋ฆฌ์•กํŠธ์˜ ๊ณ ๊ธ‰ ์ฃผ์ œ์ž…๋‹ˆ๋‹ค. HOCs๋Š” ๊ณ ์ฐจ ํ•จ์ˆ˜์™€ ๋™์ผํ•œ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค. HOCs๋Š” ํ•จ์ˆ˜์™€ ๊ฐ™์ด ์ž…๋ ฅ๊ณผ ์„ ํƒ์  ์ธ์ž๋ฅผ ๋ฐ›์•„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜ํ™˜๋œ ์ปดํฌ๋„ŒํŠธ๋Š” ์ž…๋ ฅ๋œ ์ปดํฌ๋„ŒํŠธ๋กœ JSX์—์„œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

HOCs๋Š” ์—ฌ๋Ÿฌ ์‚ฌ์šฉ ์‚ฌ๋ก€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœํผํ‹ฐ ์ค€๋น„, ์ƒํƒœ ๊ด€๋ฆฌ, ์ปดํฌ๋„ŒํŠธ ํ‘œ์‹œ ๋ณ€๊ฒฝ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜๋Š” ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์—์„œ HOCs๋ฅผ ํ—ฌํผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด List ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ๋Š”๋ฐ, ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์—†๊ฑฐ๋‚˜ null์ผ ๊ฒฝ์šฐ ์•„๋ฌด๊ฒƒ๋„ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. HOCs๋Š” ๋ฆฌ์ŠคํŠธ๊ฐ€ ์—†์„ ๋•Œ ๋ Œ๋”๋ง ํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด List ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฆฌ์ŠคํŠธ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ๋ฅผ ์ฒดํฌํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ์ € List ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ Œ๋”๋ง ํ•˜๋Š” ์—ญํ• ์—๋งŒ ์ถฉ์‹คํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž…๋ ฅ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ณ , ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ„๋‹จํ•œ HOCs๋ฅผ ๋งŒ๋“ค์–ด๋ด…์‹œ๋‹ค. src/App.js ํŒŒ์ผ์—์„œ ์ž‘์„ฑํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

function withFoo(Component) {
  return function(props) {
    return <Component { ...props } />;
  }
}

๊ด€๋ก€๋กœ HOCs ๋ณ€์ˆ˜๋ช… ์•ž์— with ์ ‘๋‘์–ด๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค. ES6 ํ™”์‚ดํ‘œ ํ•จ์ˆ˜๋กœ HOCs๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

const withFoo = (Component) => (props) =>
  <Component { ...props } />

์ด ์˜ˆ์ œ์—์„œ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋Š” ์ถœ๋ ฅ ์ปดํฌ๋„ŒํŠธ์™€ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ์•„๋ฌด ์ผ๋„ ์ผ์–ด๋‚˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋™์ผํ•œ ์ปดํฌ๋„ŒํŠธ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ  ๋ชจ๋“  props๋ฅผ ์ถœ๋ ฅ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์œ ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ถœ๋ ฅ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐœ์„ ํ•ด๋ด…์‹œ๋‹ค. ์ถœ๋ ฅ ์ปดํฌ๋„ŒํŠธ๋Š” ๋กœ๋”ฉ ์ƒํƒœ๊ฐ€ true์ผ ๋•Œ Loading ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์„ ๋•Œ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด์—ฌ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์—์„œ HOCs์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

# leanpub-start-insert
const withLoading = (Component) => (props) =>
  props.isLoading
    ? <Loading />
    : <Component { ...props } />
# leanpub-end-insert

๋กœ๋”ฉ ํ”„๋กœํผํ‹ฐ์— ๋”ฐ๋ผ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๋Š” Loading ์ปดํฌ๋„ŒํŠธ ๋˜๋Š” ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ์œ„ ์˜ˆ์ œ์ฒ˜๋Ÿผ props์™€ ๊ฐ™์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž…๋ ฅ๊ณผ ๊ฐ™์ด ์ „ํŒŒํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ๋ณด๊ณ  ์„œ๋กœ ์ฐจ์ด์ ์„ ๋น„๊ตํ•ด๋ณด์„ธ์š”.

{title="Code Playground",lang="javascript"}

// props๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ ์ „ ๊ตฌ์กฐ ํ•ด์ฒด๋ฅผ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
const { foo, bar } = props;
<SomeComponent foo={foo} bar={bar} />

// ๊ฐ์ฒด ์ „๊ฐœ ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•ด ๋ชจ๋“  ๊ฐ์ฒด ํ”„๋กœํผํ‹ฐ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
<SomeComponent { ...props } />

isLoading๋ฅผ ํฌํ•จํ•œ ๋ชจ๋“  props ๊ฐ์ฒด๋ฅผ ์ „๊ฐœํ•ด์„œ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋Š” isLoading ํ”„๋กœํผํ‹ฐ๋ฅผ ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ES6 ๊ตฌ์กฐ ํ•ด์ฒด๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

# leanpub-start-insert
const withLoading = (Component) => ({ isLoading, ...rest }) =>
  isLoading
    ? <Loading />
    : <Component { ...rest } />
# leanpub-end-insert

๊ฐ์ฒด์—์„œ ํ”„๋กœํผํ‹ฐ ํ•˜๋‚˜๋ฅผ ๊ฐ€์ ธ์˜ค์ง€๋งŒ ๋‚˜๋จธ์ง€ ๊ฐ์ฒด๋Š” ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์—ฌ๋Ÿฌ ํ”„๋กœํผํ‹ฐ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ์—ฌ๋Ÿฌ๋ถ„๋“ค์€ [MDN] ๊ตฌ์กฐ ํ•ด์ฒด ํ• ๋‹น ๋ถ€๋ถ„์„ ์ฝ์—ˆ์„ ๊ฒ๋‹ˆ๋‹ค.

์ด์ œ JSX์—์„œ HOCs๋ฅผ ์‚ฌ์šฉํ•ด๋ด…์‹œ๋‹ค. ์šฐ๋ฆฌ๋Š” "More" ๋ฒ„ํŠผ ๋˜๋Š” Loading ์ปดํฌ๋„ŒํŠธ ๋‘˜ ์ค‘ ํ•˜๋‚˜๋ฅผ ๋ณด์—ฌ์ค„ ๊ฒ๋‹ˆ๋‹ค. Loading ์ปดํฌ๋„ŒํŠธ๋Š” HOCs์— ์บก์Šํ™”๋˜์–ด์žˆ์ง€๋งŒ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋Š” ๋น ์ ธ์žˆ์Šต๋‹ˆ๋‹ค. ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋ฅผ Button ์ปดํฌ๋„ŒํŠธ๋กœ, ์ถœ๋ ฅ ์ปดํฌ๋„ŒํŠธ๋Š” ButtonWithLoading ์ปดํฌ๋„ŒํŠธ๋กœ ์ถ”๊ฐ€ํ•ฉ์‹œ๋‹ค.

{title="src/App.js",lang=javascript}

const Button = ({ onClick, className = '', children }) =>
  <button
    onClick={onClick}
    className={className}
    type="button"
  >
    {children}
  </button>

const Loading = () =>
  <div>Loading ...</div>

const withLoading = (Component) => ({ isLoading, ...rest }) =>
  isLoading
    ? <Loading />
    : <Component { ...rest } />

# leanpub-start-insert
const ButtonWithLoading = withLoading(Button);
# leanpub-end-insert

๋งˆ์ง€๋ง‰์œผ๋กœ ButtonWithLoading ์ปดํฌ๋„ŒํŠธ๊ฐ€ loading ์ƒํƒœ๋ฅผ ํ”„๋กœํผํ‹ฐ๋กœ ๊ฐ€์ ธ์˜ค๊ฒŒ ๋งŒ๋“ญ์‹œ๋‹ค. HOCs๊ฐ€ loading ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋™์•ˆ, ๋‚˜๋จธ์ง€ props ์ „์ฒด๋Š” Button ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

class App extends Component {

  ...

  render() {
    ...
    return (
      <div className="page">
        ...
        <div className="interactions">
# leanpub-start-insert
          <ButtonWithLoading
            isLoading={isLoading}
            onClick={() => this.fetchSearchTopStories(searchKey, page + 1)}>
            More
          </ButtonWithLoading>
# leanpub-end-insert
        </div>
      </div>
    );
  }
}

ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•ด๋ด…์‹œ๋‹ค. App ์ปดํฌ๋„ŒํŠธ์˜ ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•จ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ปค๋งจ๋“œ ๋ผ์ธ์—์„œ diff๊ฐ€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ณด์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

{title="Command Line",lang="text"}

-    <button
-      className=""
-      onClick={[Function]}
-      type="button"
-    >
-      More
-    </button>
+    <div>
+      Loading ...
+    </div>

์ž˜๋ชป๋˜์—ˆ๋‹ค๊ณ  ํŒ๋‹จ๋˜๋ฉด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜, ์Šค๋ƒ…์ƒท์„ ์ˆ˜๋ฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Loading ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ปค๋งจ๋“œ ๋ผ์ธ์—์„œ ๋ณ€๊ฒฝ๋œ ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜๋ฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฆฌ์•กํŠธ ๊ณ ๊ธ‰ ํ…Œํฌ๋‹‰์ž…๋‹ˆ๋‹ค. ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ, ๊ฐ•ํ™”๋œ ์ถ”์ƒํ™”, ์ปดํฌ๋„ŒํŠธ์˜ ์กฐํ•ฉ๊ณผ props, ์ƒํƒœ ๋ฐ ๋ทฐ ์กฐ์ž‘ ๋“ฑ ์—ฌ๋Ÿฌ ๋ชฉ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ ๋ชจ๋“  ๊ฒƒ์„ ์ดํ•ดํ•˜์ง€ ๋ชปํ•œ๋‹ค๊ณ  ํ•ด๋„ ๊ฑฑ์ •ํ•˜์ง€ ์•Š๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค. ์ต์ˆ™ํ•ด์ง€๊ธฐ๊นŒ์ง€ ๊ฝค ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ €์ž๊ฐ€ ์“ด ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ ์‹œ์ž‘ํ•˜๊ธฐ ๊ธ€์„ ์ฝ์–ด๋ณด๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค. ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐฐ์šฐ๋Š” ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•˜๊ณ  ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ๊ณผ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ๋‚ด ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฝ์–ด๋ณด๊ธฐ

์‹ค์Šตํ•˜๊ธฐ

  • HOCs๋ฅผ ์‹คํ—˜ํ•ด๋ด…๋‹ˆ๋‹ค.
  • ๊ทธ ์™ธ ๋” ๋งŽ์€ HOCs ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์ƒ๊ฐํ•ด๋ด…์‹œ๋‹ค.
    • ์ง์ ‘ HOCs๋ฅผ ๊ตฌํ˜„ํ•ด๋ด…์‹œ๋‹ค.

5.4 ์‹ฌํ™”: ์ •๋ ฌ

์ง€๊ธˆ๊นŒ์ง€ ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๋ชจ๋‘ ๊ฒ€์ƒ‰ ์ธํ„ฐ๋ ‰์…˜์„ ๋‹ค๋ค„๋ดค์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ์ ˆ์—์„œ๋Š” Table ์ปดํฌ๋„ŒํŠธ๋กœ ํ•œ์ธต ์‹ฌํ™”๋œ ์ธํ„ฐ๋ ‰์…˜์„ ๋‹ค๋ฃจ๊ฒ ์Šต๋‹ˆ๋‹ค. Table ์ปดํฌ๋„ŒํŠธ์˜ ๊ฐ ์ฒซ ํ–‰์— ๋ผ๋ฒจ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ด๋ฅผ ํ™œ์šฉํ•ด ์ •๋ ฌ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์ง์ ‘ ์ •๋ ฌ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์ง€๋งŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŽธํ•ด์ง‘๋‹ˆ๋‹ค. ์ด๋ฒˆ ์ ˆ์—์„œ๋Š” ๊ฐ€์žฅ ๋Œ€ํ‘œ์ ์ธ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ ๋กœ๋Œ€์‰ฌ(Lodash)๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ฌผ๋ก  ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๊ดœ์ฐฎ์Šต๋‹ˆ๋‹ค. ๋จผ์ € ๋กœ๋Œ€์‰ฌ๋ฅผ ์„ค์น˜ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="Command Line",lang="text"}

npm install lodash

src/App.js ํŒŒ์ผ์—์„œ ๋กœ๋Œ€์‰ฌ์˜ ์ •๋ ฌ ํ•จ์ˆ˜๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

import React, { Component } from 'react';
import axios from 'axios';
# leanpub-start-insert
import { sortBy } from 'lodash';
# leanpub-end-insert
import './App.css';

Table ์ปดํฌ๋„ŒํŠธ์—๋Š” title, author, comments, points ์—ด์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ •๋ ฌ ํ•จ์ˆ˜๋Š” ํŠน์ • ํ”„๋กœํผํ‹ฐ์— ๋”ฐ๋ผ ์ •๋ ฌ๋œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์ •๋ ฌ๋˜์ง€ ์•Š๋Š” ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ธฐ๋ณธ ์ •๋ ฌ ํ•จ์ˆ˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์ •๋ ฌ ํ•จ์ˆ˜๋Š” ์ดˆ๊ธฐ ์ƒํƒœ ๊ฐ’์ด ๋ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

...

# leanpub-start-insert
const SORTS = {
  NONE: list => list,
  TITLE: list => sortBy(list, 'title'),
  AUTHOR: list => sortBy(list, 'author'),
  COMMENTS: list => sortBy(list, 'num_comments').reverse(),
  POINTS: list => sortBy(list, 'points').reverse(),
};
# leanpub-end-insert

class App extends Component {
  ...
}
...

SORTS ํ•จ์ˆ˜ ์ค‘ COMMENTS์™€ POINTS ํ•จ์ˆ˜๋Š” ๋‚ด๋ฆผ์ฐจ์ˆœ ๋ฐฐ์—ด์ž…๋‹ˆ๋‹ค. COMMENTS ํ•จ์ˆ˜๋Š” ๋Œ“๊ธ€ ์ˆ˜๊ฐ€ ๋†’์€ ์ˆœ, POINTS ํ•จ์ˆ˜๋Š” ์ ์ˆ˜๊ฐ€ ๋†’์€ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋ฉ๋‹ˆ๋‹ค.

SORTS ๊ฐ์ฒด๋กœ ์ •๋ ฌ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

App ์ปดํฌ๋„ŒํŠธ์— ์ •๋ ฌ ์ƒํƒœ์ธ sortKey๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ์ •๋ ฌ ์ƒํƒœ ๊ฐ’์€ 'NONE'์ด๋ฉฐ, ์ •๋ ฌ๋˜์ง€ ์•Š์€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

this.state = {
  results: null,
  searchKey: '',
  searchTerm: DEFAULT_QUERY,
  error: null,
  isLoading: false,
# leanpub-start-insert
  sortKey: 'NONE',
# leanpub-end-insert
};

sortKey ์ƒํƒœ ๊ฐ’์„ 'AUTHOR'๋กœ ๋ฐ”๊พธ๊ฒŒ ๋˜๋ฉด, SORTS ๊ฐ์ฒด์˜ AUTHOR ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋ฆฌ์ŠคํŠธ๋ฅผ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ App ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒˆ๋กœ์šด ํด๋ž˜์Šค ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋Š” sortKey๋กœ ์ •๋ ฌ ํ•จ์ˆ˜๋ฅผ ์ฐพ์•„ ๋ฆฌ์ŠคํŠธ์— ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

class App extends Component {

  constructor(props) {

    ...

    this.needsToSearchTopStories = this.needsToSearchTopStories.bind(this);
    this.setSearchTopStories = this.setSearchTopStories.bind(this);
    this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this);
    this.onSearchSubmit = this.onSearchSubmit.bind(this);
    this.onSearchChange = this.onSearchChange.bind(this);
    this.onDismiss = this.onDismiss.bind(this);
# leanpub-start-insert
    this.onSort = this.onSort.bind(this);
# leanpub-end-insert
  }

# leanpub-start-insert
  onSort(sortKey) {
    this.setState({ sortKey });
  }
# leanpub-end-insert

  ...

}

๊ทธ๋ฆฌ๊ณ  Table ์ปดํฌ๋„ŒํŠธ์— ๋ฉ”์„œ๋“œ์™€ sortKey์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

class App extends Component {

  ...

  render() {
    const {
      searchTerm,
      results,
      searchKey,
      error,
      isLoading,
# leanpub-start-insert
      sortKey
# leanpub-end-insert
    } = this.state;

    ...

    return (
      <div className="page">
        ...
        <Table
          list={list}
# leanpub-start-insert
          sortKey={sortKey}
          onSort={this.onSort}
# leanpub-end-insert
          onDismiss={this.onDismiss}
        />
        ...
      </div>
    );
  }
}

Table ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฆฌ์ŠคํŠธ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค. sortKey ๋กœ SORT ํ•จ์ˆ˜ ์ค‘ ํ•˜๋‚˜๋ฅผ ์ทจํ•ด ๋ฆฌ์ŠคํŠธ๋ฅผ ์ž…๋ ฅ์œผ๋กœ ์ „๋‹ฌํ•˜๊ณ , ์ •๋ ฌ๋œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

# leanpub-start-insert
const Table = ({
  list,
  sortKey,
  onSort,
  onDismiss
}) =>
# leanpub-end-insert
  <div className="table">
# leanpub-start-insert
    {SORTS[sortKey](list).map(item =>
# leanpub-end-insert
      <div key={item.objectID} className="table-row">
        ...
      </div>
    )}
  </div>

์•ž์„œ ๋งํ–ˆ๋“ฏ์ด ๋ฆฌ์ŠคํŠธ๋Š” ํŠน์ • ํ•จ์ˆ˜์— ์˜ํ•ด ์ •๋ ฌ๋ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์ •๋ ฌ ๊ฐ’์€ 'NONE'์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ฆฌ์ŠคํŠธ๋Š” ์ •๋ ฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์•„์ง sortKey๋ฅผ ๋ฐ”๊ฟ”์ฃผ๋Š” onSort() ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์œผ๋กœ Sort ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด ์—ด ํ—ค๋”๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ •๋ ฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค. ๋จผ์ € Table ์•ˆ์— Sort ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

const Table = ({
  list,
  sortKey,
  onSort,
  onDismiss
}) =>
  <div className="table">
# leanpub-start-insert
    <div className="table-header">
      <span style={{ width: '40%' }}>
        <Sort
          sortKey={'TITLE'}
          onSort={onSort}
        >
          Title
        </Sort>
      </span>
      <span style={{ width: '30%' }}>
        <Sort
          sortKey={'AUTHOR'}
          onSort={onSort}
        >
          Author
        </Sort>
      </span>
      <span style={{ width: '10%' }}>
        <Sort
          sortKey={'COMMENTS'}
          onSort={onSort}
        >
          Comments
        </Sort>
      </span>
      <span style={{ width: '10%' }}>
        <Sort
          sortKey={'POINTS'}
          onSort={onSort}
        >
          Points
        </Sort>
      </span>
      <span style={{ width: '10%' }}>
        Archive
      </span>
    </div>
# leanpub-end-insert
    {SORTS[sortKey](list).map(item =>
      ...
    )}
  </div>

๋‹ค์Œ์œผ๋กœ Sort ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ •์˜ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. Sort ์ปดํฌ๋„ŒํŠธ๋Š” ํŠน์ • sortKey์™€ onSort()๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ–๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚ด๋ถ€์—์„œ ํŠน์ • ํ‚ค๋ฅผ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด sortKey ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

const Sort = ({ sortKey, onSort, children }) =>
  <Button onClick={() => onSort(sortKey)}>
    {children}
  </Button>

๋ณด์‹œ๋‹ค์‹œํ”ผ Sort ์ปดํฌ๋„ŒํŠธ๋Š” ๊ณตํ†ต์ธ Button ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹ค์‹œ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. onSort()๋ฉ”์„œ๋“œ์—์„œ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ ์ „๋‹ฌ๋œ ๊ฐ๊ฐ์˜ sortKey๊ฐ€ ์„ค์ •๋ฉ๋‹ˆ๋‹ค. ์—ด ํ—ค๋”๋ฅผ ํด๋ฆญํ•  ๋•Œ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ •๋ ฌ๋˜๋Š”์ง€ ํ™•์ธํ•ด๋ด…์‹œ๋‹ค.

์ข€๋” ์˜ˆ์˜๊ฒŒ ๊พธ๋ฉฐ์•ผํ•  ๋ถ€๋ถ„์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ์žˆ๋Š” ํ—ค๋” ๋ฒ„ํŠผ์ด ๋ณด๊ธฐ ์ข‹์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Sort ์ปดํฌ๋„ŒํŠธ ์˜†์— className์„ ์ง€์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

const Sort = ({ sortKey, onSort, children }) =>
# leanpub-start-insert
  <Button
    onClick={() => onSort(sortKey)}
    className="button-inline"
  >
# leanpub-end-insert
    {children}
  </Button>

์ด์ œ ๋ฉ‹์ง€๊ฒŒ ๋ณด์ด๋„ค์š”. ๋‹ค์Œ์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ์„ ๊ตฌํ˜„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. Sort ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‘ ๋ฒˆ ํด๋ฆญํ•˜๋ฉด ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ๋ฉ๋‹ˆ๋‹ค. ๋จผ์ € ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์„ ์‚ฌ์šฉํ•˜๋Š” isSortReverse ์ƒํƒœ๋ฅผ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค. isSortReverse๊ฐ€ true์ผ ๊ฒฝ์šฐ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ๋˜๋ฉฐ, ๋ฐ˜๋Œ€๋กœ false์ผ ๊ฒฝ์šฐ ์ •๋ ฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

this.state = {
  results: null,
  searchKey: '',
  searchTerm: DEFAULT_QUERY,
  error: null,
  isLoading: false,
  sortKey: 'NONE',
# leanpub-start-insert
  isSortReverse: false,
# leanpub-end-insert
};

onSort() ๋ฉ”์„œ๋“œ์—์„œ ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋„๋ก ๋งŒ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์กฐ๊ฑด๋ฌธ์„ ๋งŒ๋“ค์–ด sortKey ์ƒํƒœ๊ฐ’์ด ์ž…๋ ฅ๋œ sortKey์™€ ๊ฐ™์œผ๋ฉฐ isSortReverse๊ฐ€ true๋กœ ์„ค์ •๋˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ์ •๋ ฌํ•˜๊ฒŒ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

onSort(sortKey) {
# leanpub-start-insert
  const isSortReverse = this.state.sortKey === sortKey && !this.state.isSortReverse;
  this.setState({ sortKey, isSortReverse });
# leanpub-end-insert
}

Table ์ปดํฌ๋„ŒํŠธ์— isSortReverse props๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

class App extends Component {

  ...

  render() {
    const {
      searchTerm,
      results,
      searchKey,
      error,
      isLoading,
      sortKey,
# leanpub-start-insert
      isSortReverse
# leanpub-end-insert
    } = this.state;

    ...

    return (
      <div className="page">
        ...
        <Table
          list={list}
          sortKey={sortKey}
# leanpub-start-insert
          isSortReverse={isSortReverse}
# leanpub-end-insert
          onSort={this.onSort}
          onDismiss={this.onDismiss}
        />
        ...
      </div>
    );
  }
}

๋‹ค์‹œ Table ์ปดํฌ๋„ŒํŠธ์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ณ„์‚ฐํ•˜๋„๋ก ํ™”์‚ดํ‘œ ํ•จ์ˆ˜ ๋ธ”๋ก์„ ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

# leanpub-start-insert
const Table = ({
  list,
  sortKey,
  isSortReverse,
  onSort,
  onDismiss
}) => {
  const sortedList = SORTS[sortKey](list);
  const reverseSortedList = isSortReverse
    ? sortedList.reverse()
    : sortedList;

  return(
# leanpub-end-insert
    <div className="table">
      <div className="table-header">
        ...
      </div>
# leanpub-start-insert
      {reverseSortedList.map(item =>
# leanpub-end-insert
        ...
      )}
    </div>
# leanpub-start-insert
  );
}
# leanpub-end-insert

ํ—ค๋”๋ฅผ ํด๋ฆญํ•ด ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ž˜ ์ •๋ ฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณด์„ธ์š”.

๋งˆ์ง€๋ง‰์œผ๋กœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒ์„ ์œ„ํ•ด ๊ฐœ์„ ํ•  ์‚ฌํ•ญ์ด ๋‚จ์•˜์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ ์–ด๋–ค ์—ด์„ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌํ–ˆ๋Š”์ง€ ์•Œ ์ˆ˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์‹œ๊ฐ์ ์ธ ํšจ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ฐ Sort ์ปดํฌ๋„ŒํŠธ๋Š” ํŠน์ • sortKey๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์ด ํ‚ค๋กœ ํ˜„์žฌ ์„ ํƒ๋œ ์ •๋ ฌ์„ ์‹๋ณ„ํ•˜๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€ ์ƒํƒœ sortKey๋ฅผ ์„ ํƒ๋œ ์ •๋ ฌ ํ‚ค๋กœ Sort ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

const Table = ({
  list,
  sortKey,
  isSortReverse,
  onSort,
  onDismiss
}) => {
  const sortedList = SORTS[sortKey](list);
  const reverseSortedList = isSortReverse
    ? sortedList.reverse()
    : sortedList;

  return(
    <div className="table">
      <div className="table-header">
        <span style={{ width: '40%' }}>
          <Sort
            sortKey={'TITLE'}
            onSort={onSort}
# leanpub-start-insert
            activeSortKey={sortKey}
# leanpub-end-insert
          >
            Title
          </Sort>
        </span>
        <span style={{ width: '30%' }}>
          <Sort
            sortKey={'AUTHOR'}
            onSort={onSort}
# leanpub-start-insert
            activeSortKey={sortKey}
# leanpub-end-insert
          >
            Author
          </Sort>
        </span>
        <span style={{ width: '10%' }}>
          <Sort
            sortKey={'COMMENTS'}
            onSort={onSort}
# leanpub-start-insert
            activeSortKey={sortKey}
# leanpub-end-insert
          >
            Comments
          </Sort>
        </span>
        <span style={{ width: '10%' }}>
          <Sort
            sortKey={'POINTS'}
            onSort={onSort}
# leanpub-start-insert
            activeSortKey={sortKey}
# leanpub-end-insert
          >
            Points
          </Sort>
        </span>
        <span style={{ width: '10%' }}>
          Archive
        </span>
      </div>
      {reverseSortedList.map(item =>
          ...
      )}
    </div>
  );
}

์ด์ œ Sort ์ปดํฌ๋„ŒํŠธ์—์„œ sortKey์™€ activeSortKey๋ฅผ ๊ทผ๊ฑฐ๋กœ ์ •๋ ฌ์ด ํ™œ์„ฑํ™”๋˜์—ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹œ๊ฐ์ ์ธ ํšจ๊ณผ๋ฅผ ์ฃผ๊ธฐ ์œ„ํ•ด Sort ์ปดํฌ๋„ŒํŠธ์— className ์†์„ฑ์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

# leanpub-start-insert
const Sort = ({
  sortKey,
  activeSortKey,
  onSort,
  children
}) => {
  const sortClass = ['button-inline'];

  if (sortKey === activeSortKey) {
    sortClass.push('button-active');
  }

  return (
    <Button
      onClick={() => onSort(sortKey)}
      className={sortClass.join(' ')}
    >
      {children}
    </Button>
  );
}
# leanpub-end-insert

sortClass๋ฅผ ์ •์˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๊ฝค ์ข‹์•„๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. classnames ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์ด๋ฅผ ํ•ด๊ฒฐํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

classnames ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

{title="Command Line",lang="text"}

npm install classnames

๊ทธ๋ฆฌ๊ณ  src/App.js ํŒŒ์ผ์—์„œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

import React, { Component } from 'react';
import axios from 'axios';
import { sortBy } from 'lodash';
# leanpub-start-insert
import classNames from 'classnames';
# leanpub-end-insert
import './App.css';

classname ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด className ์ปดํฌ๋„ŒํŠธ๋ฅผ ์กฐ๊ฑด๋ถ€ ํด๋ž˜์Šค๋กœ ์ •์˜ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

{title="src/App.js",lang=javascript}

const Sort = ({
  sortKey,
  activeSortKey,
  onSort,
  children
}) => {
# leanpub-start-insert
  const sortClass = classNames(
    'button-inline',
    { 'button-active': sortKey === activeSortKey }
  );
# leanpub-end-insert

  return (
# leanpub-start-insert
    <Button
      onClick={() => onSort(sortKey)}
      className={sortClass}
    >
# leanpub-end-insert
      {children}
    </Button>
  );
}

๋‹ค์‹œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•ด๋ด…์‹œ๋‹ค. ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ Table ์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ–ˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•ฉ์‹œ๋‹ค. ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹ค์‹œ ๋ณ€๊ฒฝํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜๋ฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค๋งŒ, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋„ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. src/App.test.js ํŒŒ์ผ์—์„œ ์•„๋ž˜์™€ ๊ฐ™์ด Table ์ปดํฌ๋„ŒํŠธ ๋ถ€๋ถ„์— sortKey๊ณผ isSortReverse ๋ถˆ๋ฆฌ์–ธ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

{title="src/App.test.js",lang=javascript}

...

describe('Table', () => {

  const props = {
    list: [
      { title: '1', author: '1', num_comments: 1, points: 2, objectID: 'y' },
      { title: '2', author: '2', num_comments: 1, points: 2, objectID: 'z' },
    ],
# leanpub-start-insert
    sortKey: 'TITLE',
    isSortReverse: false,
# leanpub-end-insert
  };

  ...

});

Table ์ปดํฌ๋„ŒํŠธ์— ํ™•์žฅ๋œ prop๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•œ๋ฒˆ ๋” Table ์ปดํฌ๋„ŒํŠธ ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ ์‹คํŒจ๋ฅผ ์Šน์ธํ•ด์•ผํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

๋“œ๋””์–ด ์–ด๋ ค์šด ์ •๋ ฌ ๊ธฐ๋Šฅ์„ ๋ชจ๋‘ ์™„์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

์‹ค์Šตํ•˜๊ธฐ

์ฝ์–ด๋ณด๊ธฐ

{pagebreak}

5.5 ์ •๋ฆฌํ•˜๋ฉด

5์žฅ์—์„œ๋Š” ๋ฆฌ์•กํŠธ์—์„œ ์ปดํฌ๋„ŒํŠธ ๊ณ ๊ธ‰ ํ…Œํฌ๋‹‰์„ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ๊นŒ์ง€ ํ•™์Šตํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ด…์‹œ๋‹ค.

  • ๋ฆฌ์•กํŠธ
    • ref ์†์„ฑ์œผ๋กœ DOM ๋…ธ๋“œ๋ฅผ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” ๊ณ ๊ธ‰ ์ปดํฌ๋„ŒํŠธ์„ ์œ„ํ•œ ์ผ๋ฐ˜์ ์ธ ํ…Œํฌ๋‹‰์ž…๋‹ˆ๋‹ค.
    • ๋ฆฌ์•กํŠธ์—์„œ ์–ด๋ ค์šด ์ธํ„ฐ๋ ‰์…˜์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์กฐ๊ฑด๋ถ€ ํด๋ž˜์Šค๋ช…์„ ์œ„ํ•ด classnames ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ES6
    • ๊ตฌ์กฐ ํ•ด์ฒด๋กœ ๊ฐ์ฒด์™€ ๋ฐฐ์—ด์„ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

์‹ค์Šต ์ฝ”๋“œ๋Š” ๊ณต์‹ ๊นƒํ—ˆ๋ธŒ ๋ฆฌํผ์ง€ํ† ๋ฆฌ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.