Redux: ์Šค๋งˆํŠธ ๊ตฌ์„ฑ ์š”์†Œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

์— ๋งŒ๋“  2015๋…„ 08์›” 20์ผ  ยท  19์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: reduxjs/redux

๋‚˜๋Š” ๋ฌธ์„œ์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์„น์…˜์„ ์ฝ๊ณ  ์žˆ์—ˆ๊ณ  ๋ฐ”๋ณด ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์˜ˆ์ œ๊ฐ€ ์žˆ์ง€๋งŒ "์Šค๋งˆํŠธ ์ปดํฌ๋„ŒํŠธ"(connect () ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ)๋ฅผ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜์žˆ๋Š” ๊ฒƒ์€ ๋‹ค๋ฃจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์Šค๋งˆํŠธ ๊ตฌ์„ฑ ์š”์†Œ์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋Š” connect ()๊ฐ€ ๋งŒ๋“œ๋Š” ๋ž˜ํผ ๊ตฌ์„ฑ ์š”์†Œ๋กœ ์ธํ•ด ์กฐ๊ธˆ ๋” ๋ณต์žกํ•˜๋‹ค๋Š” ๊ฒƒ์ด ๋ฐํ˜€์กŒ์Šต๋‹ˆ๋‹ค. ๋ฌธ์ œ์˜ ์ผ๋ถ€๋Š” connect ()๋ฅผ ํ†ตํ•ด ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ž˜ํ•‘ํ•˜๋ ค๋ฉด 'store'prop (๋˜๋Š” ์ปจํ…์ŠคํŠธ)๊ฐ€ ์žˆ์–ด์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋‚˜๋Š” ์ด๊ฒƒ์„ํ•˜๋ ค๊ณ  ๋…ธ๋ ฅํ–ˆ๊ณ  ๊ทธ๊ฒƒ์„ ๋‹ฌ์„ฑํ•˜๋Š” ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”์ง€์— ๋Œ€ํ•œ ์•ฝ๊ฐ„์˜ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋‚ด๊ฐ€ ํ•œ ์ผ์ด ํ•ฉ๋ฆฌ์ ์œผ๋กœ ๋ณด์ด๋ฉด PR์„ ๋ฐ€์–ด์„œ ์ด์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๋ฌธ์„œ์— ์ถ”๊ฐ€ ํ•  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

์‹œ์ž‘ํ•˜๋ ค๋ฉด ๋ฌธ์„œ์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์„น์…˜์—์žˆ๋Š” ๊ธฐ์กด ์˜ˆ์ œ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๊ฐ€์ ธ ์™€์„œ connect ()๋กœ ๋ž˜ํ•‘ํ•˜์—ฌ ์ƒํƒœ ๋ฐ ๋””์ŠคํŒจ์น˜ ๋ฐ”์ธ๋”ฉ ๋œ ์ž‘์—… ์ƒ์„ฑ์ž๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค.

Header.js (์Šค๋งˆํŠธ ๊ตฌ์„ฑ ์š”์†Œ)
import React, { PropTypes, Component } from 'react';
import TodoTextInput from './TodoTextInput';
import TodoActions from '../actions/TodoActions';
import connect from 'redux-react';

class Header extends Component {
  handleSave(text) {
    if (text.length !== 0) {
      this.props.addTodo(text);
    }
  }

  render() {
    return (
      <header className='header'>
          <h1>{this.props.numberOfTodos + " Todos"}</h1>
          <TodoTextInput newTodo={true}
                         onSave={this.handleSave.bind(this)}
                         placeholder='What needs to be done?' />
      </header>
    );
  }
}

export default connect(
  (state) =>  {numberOfTodos: state.todos.length},
  TodoActions
)(Header);

๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์—์„œ๋„ ์˜ˆ์ œ์™€ ๋น„์Šทํ•ฉ๋‹ˆ๋‹ค.

Header.test.js
import expect from 'expect';
import jsdomReact from '../jsdomReact';
import React from 'react/addons';
import Header from '../../components/Header';
import TodoTextInput from '../../components/TodoTextInput';
const { TestUtils } = React.addons;

/**
 * Mock out the top level Redux store with all the required 
 * methods and have it return the provided state by default.
 * <strong i="13">@param</strong> {Object} state State to populate in store
 * <strong i="14">@return</strong> {Object} Mock store
 */
function createMockStore(state) {
  return {
    subscribe: () => {},
    dispatch: () => {},
    getState: () => {
      return {...state};
    }
  };
}

/**
 * Render the Header component with a mock store populated
 * with the provided state
 * <strong i="15">@param</strong> {Object} storeState State to populate in mock store
 * <strong i="16">@return</strong> {Object} Rendered output from component
 */
function setup(storeState) {
  let renderer = TestUtils.createRenderer();
  renderer.render(<Header store={createMockStore(storeState)} />);
  var output = renderer.getRenderedOutput();
  return output.refs.wrappedInstance();
}

describe('components', () => {
  jsdomReact();

  describe('Header', () => {
    it('should call call addTodo if length of text is greater than 0', () => {
      const output = setup({
        todos: [1, 2, 3]
      });
      var addTodoSpy = expect.spyOn(output.props, 'addTodo');

      let input = output.props.children[1];
      input.props.onSave('');
      expect(addTodoSpy.calls.length).toBe(0);
      input.props.onSave('Use Redux');
      expect(addTodoSpy.calls.length).toBe(1);
    });
  });
});

๊ด€๋ จ ๋ถ€๋ถ„ ๋งŒ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด์ด ํ…Œ์ŠคํŠธ๋ฅผ ์•ฝ๊ฐ„ ๋‹จ์ˆœํ™”ํ–ˆ์ง€๋งŒ ํ™•์‹คํ•˜์ง€ ์•Š์€ ์š”์ ์€ createMockStore ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. ํ—ค๋” ์ปดํฌ๋„ŒํŠธ๋ฅผ ์†Œํ’ˆ์—†์ด ๋ Œ๋”๋งํ•˜๋ ค๊ณ ํ•˜๋ฉด Redux (๋˜๋Š” react-redux)์—์„œ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์— store prop ๋˜๋Š” ์ปจํ…์ŠคํŠธ๊ฐ€ ์žˆ์–ด์•ผํ•œ๋‹ค๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. <Provider> ๊ตฌ์„ฑ ์š”์†Œ. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์— ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ์˜ ๋ฐฉ๋ฒ•์„ ๋งŒ๋“ค๊ณ  ํ…Œ์ŠคํŠธ๊ฐ€ ์Šคํ† ์–ด์—์„œ ์„ค์ •ํ•˜๋ ค๋Š” ์ƒํƒœ๋กœ ํ†ต๊ณผํ•˜๋„๋ก ํ—ˆ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ์ ‘๊ทผ ๋ฐฉ์‹์—์„œ ๋ณผ ์ˆ˜์žˆ๋Š” ์ด์ ์€ ๊ตฌ์„ฑ ์š”์†Œ ๋‚ด์—์„œ ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜์žˆ์„๋ฟ๋งŒ ์•„๋‹ˆ๋ผ connect ()๋กœ ์ „๋‹ฌํ•˜๋Š” ๋ฉ”์„œ๋“œ์˜ ๊ธฐ๋Šฅ๋„ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. expect(output.props.numberOfTodos).toBe(3) mapStateToProps ํ•จ์ˆ˜๊ฐ€ ๋‚ด๊ฐ€ ์˜ˆ์ƒ ํ•œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” expect(output.props.numberOfTodos).toBe(3) ์™€ ๊ฐ™์€ ๋‹ค๋ฅธ ์ฃผ์žฅ์„ ์—ฌ๊ธฐ์— ์‰ฝ๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๊ฒƒ์˜ ์ฃผ๋œ ๋‹จ์ ์€ ๋‚ด๊ฐ€ Redux ์Šคํ† ์–ด๋ฅผ ์กฐ๋กฑํ•ด์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์ธ๋ฐ, ๊ทธ๋ ‡๊ฒŒ ๋ณต์žกํ•˜์ง€๋Š” ์•Š์ง€๋งŒ ๋‚ด๋ถ€ Redux ๋กœ์ง์˜ ์ผ๋ถ€์ธ ๊ฒƒ์ฒ˜๋Ÿผ ๋Š๊ปด์ง€๊ณ  ๋ณ€๊ฒฝ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ถ„๋ช…ํžˆ ๋‚ด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์ด๋Ÿฌํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์ผ๋ฐ˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํŒŒ์ผ๋กœ ์˜ฎ๊ฒผ์œผ๋ฏ€๋กœ ์ €์žฅ์†Œ ๋ฉ”์„œ๋“œ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์ฝ”๋“œ๋ฅผ ํ•œ๊ณณ์—์„œ ์ˆ˜์ •ํ•˜๋ฉด๋ฉ๋‹ˆ๋‹ค.

์ƒ๊ฐ? ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ์Šค๋งˆํŠธ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋‹จ์œ„ ํ…Œ์ŠคํŠธํ•˜์—ฌ ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์„ ์ฐพ์•˜์Šต๋‹ˆ๊นŒ?

discussion question

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

@ernieturner @ghengeveld๊ฐ€ ์˜๋ฏธํ•˜๋Š” ๋ฐ”๋Š” ์–ด๋””์—์„œ๋‚˜ ES6 ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋ฐ์ฝ” ๋ ˆ์ดํŒ… ๋œ Header ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋‚ด๋ณด๋‚ผ ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ผ๋ฐ˜ React ๊ตฌ์„ฑ ์š”์†Œ๋Š” ์ถ”๊ฐ€ ์ด๋ฆ„ ๋‚ด๋ณด๋‚ด๊ธฐ๊ฐ€ ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด-

//header.js
export class HeaderDumbComponent {
  render() {
    return <header><div>...</div></header>;
  }
}

export default connect(
  (state) =>  {numberOfTodos: state.todos.length},
  TodoActions
)(HeaderDumbComponent);

์ด ์ด์ค‘ ๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ”์ธ ์•ฑ์—์„œ import Header from './header.js' ๋กœ ์ด์ „๊ณผ ๊ฐ™์ด ์Šค๋งˆํŠธ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ํ…Œ์ŠคํŠธ๋Š” import {HeaderDumbComponent} from '../components/header.js' ์ง์ ‘ redux๋ฅผ ์šฐํšŒํ•˜๊ณ  ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ์˜ ๋™์ž‘์„ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  19 ๋Œ“๊ธ€

๋Œ€์•ˆ์€ (์žฅ์‹๋˜์ง€ ์•Š์€) Header ํด๋ž˜์Šค๋ฅผ ๋‚ด๋ณด๋‚ด๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ๋ณ„๋„๋กœ ๊ฐ€์ ธ ์™€์„œ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ ์ตœ์†Œํ•œ Mocha๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋ชจ๋“  ํŒŒ์ผ์—์„œ jsdomReact ํ˜ธ์ถœ์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‚ด setup.js๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

import jsdom from 'jsdom';
import ExecutionEnvironment from 'react/lib/ExecutionEnvironment';

if (!global.document || !global.window) {
  global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
  global.window = document.defaultView;
  global.navigator = window.navigator;

  ExecutionEnvironment.canUseDOM = true;

  window.addEventListener('load', () => {
    console.log('JSDom setup completed: document, window and navigator are now on global scope.');
  });
}

--require ๋ช…๋ น ์ค„ ์˜ต์…˜์„ ํ†ตํ•ด๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค : mocha -r babelhook -r test/setup --recursive (babelhook์€ require('babel-core/register') ๋Œ€ํ•œ ํ˜ธ์ถœ์ž…๋‹ˆ๋‹ค).

์˜ˆ, Header ํด๋ž˜์Šค์™€ ๋ฐ์ฝ” ๋ ˆ์ดํŒ… ๋œ ํด๋ž˜์Šค๋ฅผ ๋ชจ๋‘ ๋‚ด๋ณด๋‚ผ ์ˆ˜ ์žˆ์ง€๋งŒ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•„๋„๋˜๊ธฐ๋ฅผ ๋ฐ”๋ž์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒํ•˜๋ฉด ๋ฐ์ฝ” ๋ ˆ์ดํŒ… ๋œ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ํฌํ•จ ๋œ ๋ชจ๋“  ์œ„์น˜๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค (์˜ˆ : import Header from . / components / Header` ๋Œ€์‹  import {DecoratedHeader} from './components/Header' ).

jsdom ์„ค์ •์— ๊ด€ํ•ด์„œ๋Š” ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๋ฌธ์„œ์˜ ์˜ˆ์ œ๋ฅผ ์ˆœ์ „ํžˆ ๋ณต์‚ฌํ•˜๊ณ  ์žˆ์—ˆ์œผ๋ฉฐ, ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋‹จ์ง€ ์˜ˆ์ผ๋ฟ์ž…๋‹ˆ๋‹ค.

์œ„์˜ ์ฝ”๋“œ ์ƒ˜ํ”Œ์ด ๋ถˆ์™„์ „ํ•˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ์•˜์Šต๋‹ˆ๋‹ค. ์Šค๋งˆํŠธ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์ง์ ‘ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ๋ Œ๋”๋ง ๊ฒฐ๊ณผ ๋Œ€์‹  refs.wrappedInstance ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฉ”์„œ๋“œ๊ฐ€ ์žˆ์–ด์•ผ ์—ฐ๊ฒฐ ์žฅ์‹ ๊ตฌ์„ฑ ์š”์†Œ (์œ„์˜ ์„ค์ • ํ•จ์ˆ˜์—์„œ ์—…๋ฐ์ดํŠธ ๋จ)๊ฐ€ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค. . ์ด๊ฒƒ์€ ๋‹ค์‹œ ์•ฝ๊ฐ„์˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ Redux ๋‚ด๋ถ€ (์ด ๊ฒฝ์šฐ, ํŠนํžˆ react-redux ๋‚ด๋ถ€)์— ์˜์กดํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ž‘๋™ํ•˜์ง€๋งŒ ์•ฝ๊ฐ„ ํ—ˆ์•ฝํ•˜๋‹ค๊ณ  ๋Š๋‚๋‹ˆ๋‹ค.

@ernieturner ๋˜ํ•œ์ด๋ฅผ ์œ„ํ•ด getWrappedInstance() ๊ณต์šฉ API๋ฅผ ์ œ๊ณตํ•˜๋ฏ€๋กœ refs ์ง์ ‘ ์•ก์„ธ์Šคํ•˜๋Š” ๊ฒƒ์ด ๊ฑฑ์ •๋˜๋Š” ๊ฒฝ์šฐ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด๊ฒƒ์„ ํˆฌ๋ช…ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” react-test-tree ์™€ ๊ฐ™์€ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

@ernieturner @ghengeveld๊ฐ€ ์˜๋ฏธํ•˜๋Š” ๋ฐ”๋Š” ์–ด๋””์—์„œ๋‚˜ ES6 ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋ฐ์ฝ” ๋ ˆ์ดํŒ… ๋œ Header ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋‚ด๋ณด๋‚ผ ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ผ๋ฐ˜ React ๊ตฌ์„ฑ ์š”์†Œ๋Š” ์ถ”๊ฐ€ ์ด๋ฆ„ ๋‚ด๋ณด๋‚ด๊ธฐ๊ฐ€ ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด-

//header.js
export class HeaderDumbComponent {
  render() {
    return <header><div>...</div></header>;
  }
}

export default connect(
  (state) =>  {numberOfTodos: state.todos.length},
  TodoActions
)(HeaderDumbComponent);

์ด ์ด์ค‘ ๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ”์ธ ์•ฑ์—์„œ import Header from './header.js' ๋กœ ์ด์ „๊ณผ ๊ฐ™์ด ์Šค๋งˆํŠธ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ํ…Œ์ŠคํŠธ๋Š” import {HeaderDumbComponent} from '../components/header.js' ์ง์ ‘ redux๋ฅผ ์šฐํšŒํ•˜๊ณ  ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ์˜ ๋™์ž‘์„ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@ eugene1g @ghengeveld ์ด๊ฒƒ์€ ์‹ค์ œ๋กœ ๋ฌธ์ œ์— ๋Œ€ํ•œ ์ •๋ง ์ข‹์€ ํ•ด๊ฒฐ์ฑ…์ž…๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์„ ์„ค๋ช…ํ•˜๋Š” "Writing tests"๋ฌธ์„œ์— PR์„ ๋ณด๋‚ด์‹ญ์‹œ์˜ค!

@ eugene1g ๊ทธ๊ฒŒ ๋ฐ”๋กœ ์ œ๊ฐ€ ์˜๋ฏธํ•˜๋Š”
@gaearon ๋‚ด๊ฐ€ ํ• ๊ฒŒ.

@ghengeveld ์˜คํ•ด์— ๋Œ€ํ•ด ์‚ฌ๊ณผ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ๋‚˜๋Š” ์—ฌ์ „ํžˆ CommonJS ์Šคํƒ€์ผ ๊ฐ€์ ธ ์˜ค๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ ์—ฌ์ „ํžˆ ES6 ๋ชจ๋“ˆ์—์„œ ๊ฝค ๋…น์Šฌ ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ์— ๊ณต๊ฐœํ•˜๊ธฐ์— ์ข‹์€ ์†”๋ฃจ์…˜ ์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ mapStateToProps / mapDispatchToProps ๋ฉ”์„œ๋“œ๋ฅผ ๋…ธ์ถœํ•˜์—ฌ ์ƒ์ ์„ ์กฐ๋กฑํ•˜์ง€ ์•Š๊ณ ๋„ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.

export class HeaderDumpComponent ...

export function mapStateToProps(state) ... 
export function mapDispatchToProps(dispatch) ...

export default connect(mapStateToProps, mapDispatchToProps)(HeaderDumpComponent);

๋Œ€๋ถ€๋ถ„์˜ ๋งต ๋ฐฉ๋ฒ•์€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์„ ์ •๋„๋กœ ๊ฐ„๋‹จ ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ผ๋ถ€๋Š” ๋‚ด ๊ตฌ์„ฑ ์š”์†Œ์˜ ๊ธฐ๋Šฅ์—์„œ ๋งค์šฐ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด๋ฏ€๋กœ ํ™•์ธํ•  ๊ฐ€์น˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  ์‚ฌ๋žŒ์—๊ฒŒ ์ œ์•ˆ ํ•ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๋ฌธ์„œ์— ๋Œ€ํ•œ ๋„์›€์ด ํ•„์š”ํ•˜๋ฉด ์•Œ๋ ค์ฃผ์‹ญ์‹œ์˜ค.

์ง€๊ธˆ ๋‹น์žฅ ์œ„์— ์„ค๋ช… ๋œ๋Œ€๋กœ ๊ถŒ์žฅ๋˜๋Š” ์ ‘๊ทผ ๋ฐฉ์‹์„ ์‹œ๋„ํ•˜๊ณ  ์žˆ์ง€๋งŒ ์™„์ „ํžˆ ํŽธ์•ˆํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒํ•˜๋ฉด connect ์ „๋‹ฌ ๋œ ๋งค๊ฐœ ๋ณ€์ˆ˜๊ฐ€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์™ธ๋ถ€์—์„œ ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ๋‚จ์Šต๋‹ˆ๋‹ค. ๋‚˜๋Š” ๋ชจ์˜ ์ƒ์ ์ด ์ž ์žฌ์ ์œผ๋กœ ๋ถ€์„œ ์งˆ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์ดํ•ดํ•˜์ง€๋งŒ, ์ด๊ฒƒ์ด ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ๋ฅผ ์ง€์›ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด redux-mock-store ์™€ ๊ฐ™์€ ๊ฒƒ์ด ๋›ฐ์–ด๋“ค ์ˆ˜์žˆ๋Š” ๊ณณ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ๋ฐฉํ–ฅ์— ๋Œ€ํ•ด ์–ด๋–ป๊ฒŒ ์ƒ๊ฐํ•˜์‹ญ๋‹ˆ๊นŒ?

๋ฟก๋ฟก

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

๋ฟก๋ฟก

์ด ์ €์žฅ์†Œ์—์„œ counter ๋ฐ todomvc ์˜ˆ์ œ์— ๋Œ€ํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

๋‹ค๋ฅธ ๊ตฌ์„ฑ ์š”์†Œ ๋‚ด๋ถ€์—์„œ ์Šค๋งˆํŠธ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ  ์–•์€ ๋ Œ๋”๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜์—†๋Š” ๋ฌธ์ œ๊ฐ€์žˆ์—ˆ์Šต๋‹ˆ๋‹ค (๊ตฌ์„ฑ ์š”์†Œ๋Š” componentDid * ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•จ). ์šฐ๋ฆฌ๊ฐ€ ํ•œ ๊ฒƒ์€ ๊ฐ„๋‹จํ•œ React ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด connect ํ•จ์ˆ˜๋ฅผ ์Šคํ… (sinon ์‚ฌ์šฉ)ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋ถ€์„œ์ง€๊ธฐ ์‰ฌ์šฐ ๋ฉฐ React 0.14๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•  ์ˆ˜์žˆ๊ฒŒ๋˜๋ฉด ์–•์€ ๋ Œ๋”๋Ÿฌ๋กœ ์ด๋™ํ•˜๋ ค๊ณ ํ•˜์ง€๋งŒ์ด ๋ฐฉ๋ฒ•์€ ๋‹น๋ถ„๊ฐ„ ํ…Œ์ŠคํŠธ์—์„œ ์ฐจ๋‹จ์„ ํ•ด์ œํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์—๋„ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚˜๋Š” React์™€ Redux ๋ชจ๋‘์— ๋‹ค์†Œ ์ต์ˆ™ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ด๊ฒƒ์ด ๋‚˜๋ฅผ ๋ฐฉํ•ดํ•˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์–ด๋–ป๊ฒŒ ๋Œ์•„ ๋‹ค๋‹ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๊นŒ? ๋ฟก๋ฟก

์ œ์•ˆ ๋œ ์ด์ค‘ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฐฉ๋ฒ•์€ select (mapStoreToState) ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ๋‘ก๋‹ˆ๋‹ค. ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ๋‚ด๋ณด๋‚ด๊ธฐ๋„ํ•ด์•ผํ•˜์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ด๋ฆ„์€ ๋˜ ๋ณ€๊ฒฝํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

๋‚˜๋Š” shallowRenderer๊ฐ€ ์—ฐ๊ฒฐ์˜ ๋ž˜ํ•‘ ๋œ ๊ตฌ์„ฑ ์š”์†Œ์™€ ํ•จ๊ป˜ ์ž‘๋™ํ•˜๋„๋กํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ๋Š” ๋ฐ ๊ด€์‹ฌ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚ด ํ˜„์žฌ ๋ฌธ์ œ๋Š” ์–•์€ ๋ Œ๋”๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์˜ˆ์ƒ๋˜๋Š” Connect ๊ตฌ์„ฑ ์š”์†Œ ๋งŒ ์ „๋‹ฌํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ œ์•ˆ ๋œ ์ด์ค‘ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฐฉ๋ฒ•์€ select (mapStoreToState) ํ•จ์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ๋‘ก๋‹ˆ๋‹ค. ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ๋‚ด๋ณด๋‚ด๊ธฐ๋„ํ•ด์•ผํ•˜์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ด๋ฆ„์€ ๋˜ ๋ณ€๊ฒฝํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

์—„๊ฒฉํžˆ "ํ…Œ์ŠคํŠธ์˜ ์ด๋ฆ„์œผ๋กœ"๋Š” ์•„๋‹™๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๋ฆฌ๋“€์„œ์™€ ํ•จ๊ป˜ ์„ ํƒ๊ธฐ ํ•จ์ˆ˜ (๊ฒฐ๊ตญ mapStateToProps ๊ฐ€ ๋ฌด์—‡์ธ์ง€)๋ฅผ ์ •์˜ํ•˜๊ณ  ํ•จ๊ป˜ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์ด ๋กœ์ง์€ UI์™€ ๋ฌด๊ด€ํ•˜๋ฉฐ ๋ฒˆ๋“ค๋กœ ์ œ๊ณต ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ผ๋ถ€ ์„ ํƒ๊ธฐ์— ๋Œ€ํ•œ shopping-cart ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด์‹ญ์‹œ์˜ค.

๊ทธ๋ž˜์„œ @gaearon ,์ด _selector_ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํƒœ์—์„œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๊ฒƒ์„ ์ œ์•ˆํ•ฉ๋‹ˆ๊นŒ? ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์‚ฌ๋žŒ๋“ค์€ ์ƒํƒœ์—์„œ ์—ฌ๋Ÿฌ ์†์„ฑ์„ ์ฝ๊ณ  ์ปดํฌ๋„ŒํŠธ์˜ ์†Œํ’ˆ์— ํ• ๋‹นํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ถˆํ•„์š”ํ•œ ์˜ค๋ฒ„ ํ—ค๋“œ๊ฐ€ ๋งŽ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๊นŒ?

์˜ˆ, Redux์— ๋Œ€ํ•œ ์ผ๋ฐ˜์ ์ธ ์ œ์•ˆ ํŒจํ„ด์€ state.some.nested.field ์ง์ ‘ ์•ก์„ธ์Šคํ•˜๋Š” ๋Œ€์‹  ๊ฑฐ์˜ ๋ชจ๋“  ๊ณณ์—์„œ ์„ ํƒ๊ธฐ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋งค์šฐ ๊ฐ„๋‹จํ•œ "์ผ๋ฐ˜"ํ•จ์ˆ˜์ผ ์ˆ˜ ์žˆ์ง€๋งŒ ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์œผ๋กœ ๋ฉ”๋ชจ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” Reselect ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•จ๊ป˜ ๊ฒฐํ•ฉ๋ฉ๋‹ˆ๋‹ค.

์ถ”๊ฐ€ ์˜ค๋ฒ„ ํ—ค๋“œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

์—ฌ๊ธฐ์— ์„ค๋ช… ๋œ๋Œ€๋กœ ์ด์ค‘ ๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ์ˆ˜ํ–‰ํ–ˆ์ง€๋งŒ connect ์†Œ์Šค๋ฅผ ์ฝ๊ณ  "Container"๊ฐ€ WrappedComponent ์ •์  ์†์„ฑ์˜ "dumb"๊ตฌ์„ฑ ์š”์†Œ์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ์œ ์ง€ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋Œ€์‹  :

// header.js
export const HeaderDumbComponent = (props) => <header><div>...</div></header>
export default connect(mapStateToProps)(HeaderDumbComponent)


// header.spec.js
import { HeaderDumbComponent } from './header'

it('renders', () => {
  expect(<HeaderDumbComponent />).to.not.be.null
})

"dumb"๋ฒ„์ „์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ WrappedComponent ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด์ค‘ ๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ๋ฐฉ์ง€ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// header.js
const HeaderDumbComponent = (props) => <header><div>...</div></header>
export default connect(mapStateToProps)(HeaderDumbComponent)


// header.spec.js
import Header from './header'

it('renders', () => {
  expect(<Header.WrappedComponent />).to.not.be.null
})

@gaearon- WrappedComponent ๋Š” ๋ฌธ์„œ์—์žˆ๋Š” ๊ฒƒ์„ ๊ฐ์•ˆํ•  ๋•Œ ๊ฝค ์•ˆ์ •์ ์ธ ์†์„ฑ ์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์–ด๋–ค ์ด์œ ๋กœ ๋“  ์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉํ•˜์ง€ ๋ง๋ผ๊ณ  ์กฐ์–ธ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

๋‚ด๊ฐ€ํ•˜๋Š” ์ผ์€ ์–ธ ๋ž˜ํ•‘ ๋œ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋‚ด๋ณด๋‚ด๊ณ  mapStateToProps ๋ฐ mapDispatchToProps๋ฅผ ๋‚ด๋ณด๋‚ด ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ๋„๋กํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

import {ManageCoursePage, mapStateToProps, mapDispatchToProps} from './ManageCoursePage';
describe("mapStateToProps", () => {
    function setup(courses, authors, id) {
        const state = {
            authors: authors
        };

        const ownProps = {
            params: {
                id: id
            }
        };

        return mapStateToProps(state, ownProps);
    }

    it('sets the course when a course id is set', () => {
        const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
        const authors = [];
        const id = "1";

        const props = setup(courses, authors, id);

        expect(props.course).toBe(courses[0]);
    });

    it('sets the course when a course id is not set', () => {
        const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
        const authors = [];

        const props = setup(courses, authors, null);

        expect(props.course).toEqual({});
    });

    it('sets the course when a course id is set that is not present in the courses list', () => {
        const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
        const authors = [];
        const id = "42";

        const props = setup(courses, authors, null);

        expect(props.course).toEqual({});
    });

    it('sets the authors formatted for a drop down list', () => {
        const courses = [];
        const authors = [ { id: "1", name: "John" }, { id: "2", name: "Jill" }];

        const props = setup(courses, authors, null);

        expect(props.authors).toEqual([
            { value: "1", text: "John" },
            { value: "2", text: "Jill" }
        ])
    });
});
์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰