Redux: ๊ณต๊ธ‰์ž์— ๋ž˜ํ•‘๋˜๊ฑฐ๋‚˜ Enzyme๊ณผ ์—ฐ๊ฒฐํ•˜์—ฌ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ฐธ์กฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

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

<Provider> ๋ฐ connect ๋กœ ๋ž˜ํ•‘๋œ ํ•ญ๋ชฉ์„ ์ฐธ์กฐํ•  ์ˆ˜ ์—†๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

// test
let component = shallow(<Provider store={store}><ContainerComponent /></Provider>);
component.find('#abc'); // returns null

let component = shallow(<Provider store={store}><div id="abc"></div></Provider>);
component.find('#abc'); // returns the div node

// ContainerComponent
const Component = ({...}) => (<div id="abc"></div>);
export default connect(..., ...)(Component);

๋‚˜๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ํšจ์†Œ๋กœ ์ž‘์„ฑ๋œ ์˜ˆ์ œ์— https://github.com/reactjs/redux/issues/1481 ์„ ๋”ฐ๋ž์ง€๋งŒ ์ปจํ…Œ์ด๋„ˆ๋Š” ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์Šค๋งˆํŠธ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•˜๋Š”์ง€/ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค.

@fshowalter ์–ด๋–ค ์•„์ด๋””์–ด๊ฐ€ ์žˆ์Šต๋‹ˆ๊นŒ?

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

Redux์™€ ์ง์ ‘์ ์ธ ๊ด€๋ จ์€ ์—†์ง€๋งŒ. ์ปจํ…Œ์ด๋„ˆ์—์„œ shallow() ๋ฅผ ๋‹ค์‹œ ํ˜ธ์ถœํ•˜์—ฌ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์Šคํ† ์–ด ์ƒํƒœ๊ฐ€ ์ „๋‹ฌ๋œ ๋‚ด๋ถ€ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์‹œ:

import React from 'react';
import {expect} from 'chai';
import {shallow} from 'enzyme';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import events from 'events';

const mockStore = configureMockStore([ thunk ]);
const storeStateMock = {
  myReducer:{
    someState: 'ABC'
  }
};

let store;
let component;
describe('tests for MyContainerComponent', () => {
  beforeEach(() => {
    store = mockStore(storeStateMock);
    component = shallow(<MyContainerComponent store={store} />).shallow();
  });

  it('renders container', () => {
    expect(component.find('div.somediv')).to.have.length.of(1);
  });
  ...
});

๋„์›€์ด ๋˜์—ˆ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค

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

connect() ๋„ Provider ๋„ ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ผ๋ถ€๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ๋Œ€์‹  ์ ์ ˆํ•œ ์ €์žฅ์†Œ( https://github.com/reactjs/react-redux)์— ๋ฌธ์ œ๋ฅผ ์ œ์ถœํ•˜๋ฉด ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ๋…ผ์˜ํ•˜๊ณ  ์ถ”์ ํ•˜๋Š” ๊ฒƒ์ด ๋” ์‰ฝ์Šต๋‹ˆ๋‹ค.

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

๋‘ ๊ฐ€์ง€ ์ƒ๊ฐ:

๋จผ์ € connect() ์— ์˜ํ•ด ์ƒ์„ฑ๋œ ๋ž˜ํผ ๊ตฌ์„ฑ ์š”์†Œ๋Š” $ context.store ๋ฅผ ์ฐพ๊ธฐ ์ „์— ์‹ค์ œ๋กœ props.store ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์‹ค์ œ๋กœ ์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค๋ฉด , ์ œ๊ณต์ž๋‚˜ ์ปจํ…์ŠคํŠธ ๋“ฑ์— ๋Œ€ํ•ด ๊ฑฑ์ •ํ•  ํ•„์š” ์—†์ด <ConnectedComponent store={myTestStore} /> ๋ฅผ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‘ ๋ฒˆ์งธ ์งˆ๋ฌธ์€ ์™„์ „ํžˆ ์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์‹ค์ œ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•ด ์ •๋ง๋กœ ๊ฑฑ์ •ํ•  ํ•„์š”๊ฐ€ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€์ž…๋‹ˆ๋‹ค. ๋‚ด๊ฐ€ ๋ณธ ์ฃผ์žฅ์€ "์ผ๋ฐ˜" ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํŠน์ • props๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ณ  mapStateToProps ๊ตฌํ˜„์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด react-redux๊ฐ€ ์ด๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ฒฐํ•ฉํ•  ๊ฒƒ์ด๋ผ๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ๊ฐ€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ๋œ ๋ฒ„์ „ ์ž์ฒด๋ฅผ ์‹ค์ œ๋กœ ํ…Œ์ŠคํŠธํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

@gaearon ๋งž์Šต๋‹ˆ๋‹ค ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜์‘ ๋˜๋Š” ํšจ์†Œ ์ €์žฅ์†Œ์—์„œ ์ด๊ฒƒ์„ ์˜ฌ๋ฆด์ง€ ์—ฌ๋ถ€๋ฅผ ๋ชฐ๋ž์Šต๋‹ˆ๋‹ค.

@markerikson ์Šค๋งˆํŠธ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ์ด์œ ๋Š” mapToDispatchProps ์— ๋Œ€ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ConnectedComponent ์— ์ƒ์ ์„ ์ „๋‹ฌํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ์ƒํƒœ ๋งคํ•‘์ด๋‚˜ ๋””์ŠคํŒจ์น˜ ์ฝœ๋ฐฑ ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด์— ๋Œ€ํ•ด ๋” ์ƒ๊ฐํ•ด๋ณด๊ณ  ํ•„์š”ํ•˜๋ฉด ๊ด€๋ จ ๋ ˆํฌ์—์„œ ๋ฌธ์ œ๋ฅผ ์ œ๊ธฐํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๋„์™€ ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

@mordra : "์˜ฌ๋ฐ”๋ฅธ ๋””์ŠคํŒจ์ฒ˜๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"๋ผ๊ณ  ๋งํ•˜๋ฉด ์‹ค์ œ๋กœ "์˜ฌ๋ฐ”๋ฅธ ์ž‘์—… ์ž‘์„ฑ์ž"๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๊นŒ?

๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(๊ตฌ๋ฌธ์€ ์•„๋งˆ๋„ ๊บผ์ ธ ์žˆ์ง€๋งŒ ์•„์ด๋””์–ด๋Š” ์–ป์–ด์•ผ ํ•จ).

let actionSpy = sinon.spy();

let wrapper = shallow(<PlainComponent someActionProp={actionSpy} />);
wrapper.find(".triggerActionButton").simulate("click");
expect(actionSpy.calledOnce).to.be.true;
expect(actionSpy.calledWith({type : ACTION_I_WAS_EXPECTING)).to.be.true;

๋‹ค์‹œ ๋งํ•ด, ์ผ๋ฐ˜ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ  mapDispatch ์—์„œ ๋ฐ˜ํ™˜๋œ ์ž‘์—…์ด ๋  ์†Œํ’ˆ์— ๋Œ€ํ•œ ์ŠคํŒŒ์ด๋ฅผ ์ „๋‹ฌํ•˜๊ณ  ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ํ•ด๋‹น ์ž‘์—…์„ ํ˜ธ์ถœํ•˜๋„๋ก ํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ๋™์ž‘์„ ํŠธ๋ฆฌ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ ์ด์ „ ์˜๊ฒฌ์— ๋”ฐ๋ฅด๋ฉด mapStateToProps ๋ฐ mapDispatchToProps ๋ฅผ ๊ฐœ๋ณ„์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋ฉฐ ์—ฐ๊ฒฐ๋œ ๋ฒ„์ „์„ ํ…Œ์ŠคํŠธํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜์ง€ ์•Š๊ณ ๋„ React-Redux๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ํ˜ธ์ถœํ•  ๊ฒƒ์ด๋ผ๋Š” ํ™•์‹ ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ์ž์ฒด๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

@markerikson ๋‚ด PlainComponent ๋Š” ์•ก์…˜์ด๋‚˜ ์•ก์…˜ ์ œ์ž‘์ž์— ๋Œ€ํ•ด ์•Œ์ง€ ๋ชปํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹น์‹ ์ด ์ œ์•ˆํ•˜๋Š” ๊ฒƒ์„ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•์ธ์ง€๋Š” ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ ๋‹ค์Œ์„ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
https://github.com/mordra/cotwmtor/blob/master/client/charCreation/charCreation.jsx
๋‚ด mapDispatchToProps ์— ๋ชจ๋“  ๋…ผ๋ฆฌ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const mapDispatchToProps = (dispatch) => {
  return {
    onCompleted      : (player) => {
      Meteor.call('newGame', player, function (data) {
        console.log('new game return: ' + data);
      });
      dispatch(routeActions.push('/game'));
      dispatch({type: "INIT_GAME", map: generateAreas(), buildings: generateBuildings(dispatch)});
    },
    onCancelled      : () => dispatch(routeActions.push('/')),
    onChangeName     : input => dispatch(actions.changeName(input)),
    onChangeGender   : gender => dispatch(actions.setGender(gender)),
    onSetDifficulty  : lvl => dispatch(actions.setDifficulty(lvl)),
    onChangeAttribute: (attr, val) => dispatch(actions.setAttribute(attr, val))
  }
};

๊ทธ๋ž˜์„œ PlainComponent์— ์ „๋‹ฌ๋œ props๋ฅผ ์—ผํƒํ•˜๋ฉด ์˜ฌ๋ฐ”๋ฅธ ์กฐ์น˜๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์—†์ง€๋งŒ, ์˜คํžˆ๋ ค ์กฐ์น˜ ์ž‘์„ฑ์ž์—๊ฒŒ ์ „๋‹ฌ๋œ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ์ง€ ์—ฌ๋ถ€๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
๊ทธ๋Ÿฐ ๋‹ค์Œ connect ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ณ„๋„๋กœ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์•„. ๊ทธ๊ฒƒ์€ ๋‚ด๊ฐ€ ๋” ๋งŽ์€ ๊ฒƒ์„ ์ดํ•ดํ•˜๋Š” ๋ฐ ๋„์›€์ด๋ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์‹ค์ œ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์‹ค์ œ ๊ฒฝํ—˜์ด ๊ฑฐ์˜ ์—†๋‹ค๋Š” ๊ฒฝ๊ณ ์™€ ํ•จ๊ป˜ ๋ช‡ ๊ฐ€์ง€ ์ƒ๊ฐ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์‹ค์ œ๋กœ ์•ก์…˜ ์ƒ์„ฑ์ž๊ฐ€ ์žˆ๋Š” ๊ฒƒ์€ dispatch ์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„๋กœ ์ •์˜๋˜์ง€ ์•Š์•˜์„ ๋ฟ์ž…๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์€ ๊ทธ ์ž์ฒด๋กœ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์•„๋งˆ๋„ ๊ทธ๋“ค์˜ ํ–‰๋™์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ธฐ๋ฅผ ์›ํ•  ๊ฒƒ์ด๊ณ , ๊ทธ๋ ‡๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด mapDispatch ์™ธ๋ถ€์˜ ์ž์ฒด ํ•จ์ˆ˜๋กœ ์ •์˜ํ•˜๊ธฐ๋ฅผ ์›ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  • ์ด๊ฒƒ๋“ค์€ ๋ชจ๋‘ redux-thunk์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ์— ์•„์ฃผ ์ข‹์€ ํ›„๋ณด์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ํ™•์‹คํžˆ ์ด๋Ÿฌํ•œ ํ•จ์ˆ˜๋ฅผ ์ž์ฒด์ ์œผ๋กœ ์ •์˜ํ•˜๊ณ  dispatch ์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ท€ํ•˜์˜ PlainComponent๋Š” _does_ "์•ก์…˜"์— ๋Œ€ํ•ด ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜๋Š” ์ด ๊ฒฝ์šฐ ์ตœ์†Œํ•œ ์ฝœ๋ฐฑ์„ ์ „๋‹ฌํ•˜๊ณ  ์žˆ๋‹ค๋Š” ์ ์—์„œ, ๊ทธ๋ฆฌ๊ณ  ์ปดํฌ๋„ŒํŠธ์˜ ์–ด๋”˜๊ฐ€์—์„œ this.props.onSetDifficulty("HARD") ๋˜๋Š” ์ด์™€ ์œ ์‚ฌํ•œ ๊ฒƒ์„ ์‹คํ–‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚ด๊ฐ€ ํ–‰๋™์„ ์œ„ํ•ด ์ŠคํŒŒ์ด๋ฅผ ๋ณด๋‚ผ ๊ฒƒ์„ ์ œ์•ˆํ–ˆ์„ ๋•Œ, ์ด๊ฒƒ์€ ๋‚ด๊ฐ€ ๊ต์ฒด๋ฅผ ์ œ์•ˆํ•œ ์ข…๋ฅ˜์˜ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ onSetDifficulty ์— ๋Œ€ํ•œ ์ŠคํŒŒ์ด๋ฅผ ์ „๋‹ฌํ•œ ๊ฒฝ์šฐ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์ด๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ๋‚œ์ด๋„ ์ˆ˜์ค€์— ๋Œ€ํ•ด ํ—ˆ์šฉ๋˜๋Š” ๊ฐ’์„ ์ „๋‹ฌํ–ˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

๋˜ํ•œ ๊ตฌ์„ฑ ์š”์†Œ์˜ ๊ด€์ ์—์„œ ๋ณด๋ฉด ๊ถ๊ทน์ ์œผ๋กœ {action: CHANGE_NAME, name : "Fred"} ๊ฐ€ ๋ฐœ์†ก๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€์— ๋Œ€ํ•ด ์‹ค์ œ๋กœ ๊ฑฑ์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์ด ์•„๋Š” ๊ฒƒ์€ this.props.onChangeName("Fred") ๋ผ๊ณ  ํ–ˆ๊ณ  _that_ ์€ ๋‹น์‹ ์ด ํ…Œ์ŠคํŠธํ•˜๋ ค๊ณ  ์‹œ๋„ํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค - ๊ทธ๊ฒƒ์ด ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ prop ์ฝœ๋ฐฑ์„ ํ˜ธ์ถœํ–ˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

@mordra ๊ท€ํ•˜์˜ ๊ฒฝ์šฐ mapDispatchToProps ๋ฅผ ๋‚ด๋ณด๋‚ด๊ณ  ๊ฒฉ๋ฆฌํ•˜์—ฌ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. ์†์„ฑ ์ด๋ฆ„์ด ๋ชจ๋‘ ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธํ•˜๊ณ  ์ž‘์—… ์ž‘์„ฑ์ž๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด ํŒŒ๊ฒฌ์šฉ ์ŠคํŒŒ์ด๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฆ‰, ๋‚˜๋Š” mapActionCreatorsToProps๋ฅผ ์„ ํ˜ธํ•˜์—ฌ mapDispatchToProps ๋ฅผ ํ”ผํ•˜๋Š” ๊ฒฝํ–ฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค. mapActionCreatorsToProps ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” ์ด๋ฏธ ํ˜•์„ฑ๋œ ์•ก์…˜ ์ œ์ž‘์ž๋ฅผ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ๋ชจ๋“  ์†Œํ’ˆ์ด ๊ฐ€์ ธ์˜ค๊ธฐ ์˜คํƒ€๋ฅผ ๋ฐฉ์ง€ํ•˜๋„๋ก ์ •์˜๋˜์–ด ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.

๋งˆ์ง€๋ง‰์œผ๋กœ ์ผ๋ฐ˜(์–•์ง€ ์•Š์€) ๋ Œ๋”๋ง์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” jsdom์ด๋‚˜ ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ•„์š”ํ•˜์ง€๋งŒ ๋ชจ๋“  ์ˆ˜์ค€์—์„œ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ˜์‘ ๋˜๋Š” ํšจ์†Œ ์ €์žฅ์†Œ์—์„œ ์ด๊ฒƒ์„ ์˜ฌ๋ฆด์ง€ ์—ฌ๋ถ€๋ฅผ ๋ชฐ๋ž์Šต๋‹ˆ๋‹ค.

์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๋‚˜๋Š” React ๋˜๋Š” Enzyme repos๋ฅผ ์˜๋ฏธํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‚˜๋Š” ๋‹น์‹ ์ด ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ React Redux ๋ฅผ ์˜๋ฏธํ–ˆ์Šต๋‹ˆ๋‹ค.

๋„์›€์„ ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๋‚ด๊ฐ€ ๊ฐ€์„œ ์†Œํ™”ํ•ด์•ผ ํ•  ๋งŽ์€ ์ •๋ณด์ž…๋‹ˆ๋‹ค. @markerikson @fshowalter ์ฃผ๋ณ€์„ ์ข€ ๋” ๋‘˜๋Ÿฌ๋ณด๊ณ  mapState/Dispatch๋ฅผ ๋‚ด๋ณด๋‚ด๊ณ  ๋ณ„๋„๋กœ ํ…Œ์ŠคํŠธํ•˜์ž๋Š” ์ œ์•ˆ์„ ๋ฐ›์•„๋“ค์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์˜ˆ์ƒ๋˜๋Š” ๋™์ž‘์„ ์œ ๋ฐœํ•˜๋Š” ์ฝœ๋ฐฑ์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด ํ•ฉ๋ฆฌ์ ์ž…๋‹ˆ๋‹ค.

@gaearon Stateless ๊ตฌ์„ฑ ์š”์†Œ๋Š” ์–•๊ฒŒ ๋ฌธ์ œ๋ฅผ ํ›‘์–ด๋ณด์ง€ ์•Š๊ณ  ๋ Œ๋”๋ง๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ค‘์ฒฉ๋œ stateless ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์žˆ๋Š” stateful ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐ ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ง€๊ธˆ์€ ๊ทธ ๊ฒฝ๋กœ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๋ฉ”๋ชจ๋ฅผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

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

@gaearon : ๋ช…ํ™•ํžˆ ํ•˜๊ธฐ ์œ„ํ•ด ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด:

let wrapper = shallow(<A><B /></A>);

B๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๊นŒ, ์•„๋‹ˆ๋ฉด ๋ Œ๋”๋ง๋˜์ง€ ์•Š์Šต๋‹ˆ๊นŒ? ๊ทธ๊ฒƒ์ด ์›๋ž˜ ์งˆ๋ฌธ์ด์—ˆ๋˜ ๊ฒƒ ๊ฐ™์•„์š” - ์—ฐ๊ฒฐ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด ์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ ์ฃผ์œ„์— <Provider> ๋ฅผ ๋ Œ๋”๋งํ•˜๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค.

Redux์™€ ์ง์ ‘์ ์ธ ๊ด€๋ จ์€ ์—†์ง€๋งŒ. ์ปจํ…Œ์ด๋„ˆ์—์„œ shallow() ๋ฅผ ๋‹ค์‹œ ํ˜ธ์ถœํ•˜์—ฌ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์Šคํ† ์–ด ์ƒํƒœ๊ฐ€ ์ „๋‹ฌ๋œ ๋‚ด๋ถ€ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์‹œ:

import React from 'react';
import {expect} from 'chai';
import {shallow} from 'enzyme';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import events from 'events';

const mockStore = configureMockStore([ thunk ]);
const storeStateMock = {
  myReducer:{
    someState: 'ABC'
  }
};

let store;
let component;
describe('tests for MyContainerComponent', () => {
  beforeEach(() => {
    store = mockStore(storeStateMock);
    component = shallow(<MyContainerComponent store={store} />).shallow();
  });

  it('renders container', () => {
    expect(component.find('div.somediv')).to.have.length.of(1);
  });
  ...
});

๋„์›€์ด ๋˜์—ˆ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค

์ปค์Šคํ…€ ๋ชจ๋“ˆ์„ ํ†ตํ•ด ๋” ์ ‘๊ทผํ•˜๊ธฐ ์‰ฌ์šด ๋ฐฉ๋ฒ•์„ ๋งŒ๋“ค์–ด์•ผ ํ• ๊นŒ์š”?

ํŽธ์ง‘: ์—ฐ๊ฒฐ์„ component = shallowWithConnect() ์™€ ๊ฐ™์€ Enzyme์— ๋Œ€ํ•œ ํ•˜๋‚˜์˜ ๋ฐฉ๋ฒ•์œผ๋กœ ์„ ์–ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@ev1stensberg ์ด๊ฒƒ์€ ์ข‹์€ ์ƒ๊ฐ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์ ‘๊ทผ ๋ฐฉ์‹์ธ์ง€ ๋ฐ/๋˜๋Š” ์ด์— ๋Œ€ํ•œ ์ž‘์—…์ด ์‹œ์ž‘๋˜์—ˆ๋Š”์ง€ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๊นŒ? ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด ๊ธฐ์—ฌํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

๋ฏธ๋ž˜์— ์ด ๋ฌธ์ œ๋ฅผ ์šฐ์—ฐํžˆ ๋ฐœ๊ฒฌํ•œ ์‚ฌ๋žŒ์„ ์œ„ํ•ด ์ €์—๊ฒŒ ์ ํ•ฉํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์ž์ฒด์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๊ตฌ์„ฑ ์š”์†Œ ์ •์˜๋ฅผ ๋ช…๋ช…๋œ ๋‚ด๋ณด๋‚ด๊ธฐ๋กœ ๋‚ด๋ณด๋‚ด๊ณ  ์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ(์•ฑ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด)๋ฅผ ๊ธฐ๋ณธ ๋‚ด๋ณด๋‚ด๊ธฐ๋กœ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค. ์›๋ž˜ ์งˆ๋ฌธ์˜ ์ฝ”๋“œ๋กœ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.

// test
let component = shallow(<Provider store={store}><ContainerComponent /></Provider>);
component.find('#abc'); // returns null

let component = shallow(<Provider store={store}><div id="abc"></div></Provider>);
component.find('#abc'); // returns the div node

// ContainerComponent
export const Component = ({...}) => (<div id="abc"></div>); // I EXPORT THIS, TOO, JUST FOR TESTING
export default connect(..., ...)(Component);

๊ทธ๋Ÿผ ๊ทธ๋ƒฅ

const component = shallow(<Component {...props} />)
// test component

๊ทธ๋ฆฌ๊ณ  ์œ„์—์„œ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์ด ์ง€์ ํ–ˆ๋“ฏ์ด ์—ฐ๊ฒฐ์— ์ „๋‹ฌํ•˜๋Š” ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ์ ์šฉ ๋ฒ”์œ„๊ฐ€ ์žˆ๋‹ค๋ฉด ๊ตฌ์„ฑ ์š”์†Œ ์ „์ฒด์— ๋Œ€ํ•œ ์ „์ฒด ์ ์šฉ ๋ฒ”์œ„๊ฐ€ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ž, ์—ฌ๊ธฐ์„œ ๊ฐ€์žฅ ๋จผ์ € ์ฃผ๋ชฉํ•ด์•ผ ํ•  ๊ฒƒ์€ :

  1. ์ƒ์ ์—์„œ ์ƒํƒœ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  2. ๊ธฐ๋ฐ˜์œผ๋กœ ๋””์ŠคํŒจ์น˜ ์ž‘์—…์„ ํ†ตํ•ด ์ƒํƒœ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ.

๋‚ด๊ฐ€ ๊ดœ์ฐฎ๋‹ค๋ฉด ๋‹ค์Œ ๋‘ ๊ฐ€์ง€๋งŒ ํ…Œ์ŠคํŠธํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

  1. ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์ƒํƒœ์—์„œ ์ƒ์„ฑ๋œ ์˜ฌ๋ฐ”๋ฅธ ์†Œํ’ˆ์„ ์ˆ˜์‹ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๊นŒ(์„ ํƒ์ž๋ฅผ ํ†ตํ•ด ๊ฐ€๋Šฅ)?
  2. ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ์ž‘์—…์„ ์ „๋‹ฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๊นŒ?

์ด๊ฒƒ์ด ๋‚ด ์ ‘๊ทผ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

import React from 'react';
import { shallow } from 'enzyme';
import { fromJS } from 'immutable';
import { createStore } from 'redux';
// this is the <Map /> container
import Map from '../index';
// this is an action handled by a reducer
import { mSetRegion } from '../reducer';
// this is an action handled by a saga
import { sNewChan } from '../sagas';
// this is the component wrapped by the <Map /> container
import MapComponent from '../../../components/Map';

const region = {
  latitude: 20.1449858,
  longitude: -16.8884463,
  latitudeDelta: 0.0222,
  longitudeDelta: 0.0321,
};
const otherRegion = {
  latitude: 21.1449858,
  longitude: -12.8884463,
  latitudeDelta: 1.0222,
  longitudeDelta: 2.0321,
};
const coordinate = {
  latitude: 31.788,
  longitude: -102.43,
};
const marker1 = {
  coordinate,
};
const markers = [marker1];
const mapState = fromJS({
  region,
  markers,
});
const initialState = {
  map: mapState,
};
// we are not testing the reducer, so it's ok to have a do-nothing reducer
const reducer = state => state;
// just a mock
const dispatch = jest.fn();
// this is how i mock the dispatch method
const store = {
  ...createStore(reducer, initialState),
  dispatch,
};
const wrapper = shallow(
  <Map store={store} />,
);
const component = wrapper.find(MapComponent);

describe('<Map />', () => {
  it('should render', () => {
    expect(wrapper).toBeTruthy();
  });

  it('should pass the region as prop', () => {
    expect(component.props().region).toEqual(region);
  });

  it('should pass the markers as prop', () => {
    expect(component.props().markers).toEqual(markers);
  });
  // a signal is an action wich will be handled by a saga
  it('should dispatch an newChan signal', () => {
    component.simulate('longPress', { nativeEvent: { coordinate } });
    expect(dispatch.mock.calls.length).toBe(1);
    expect(dispatch.mock.calls[0][0]).toEqual(sNewChan(coordinate));
  });
  // a message is an action wich will be handled by a reducer
  it('should dispatch a setRegion message', () => {
    component.simulate('regionChange', otherRegion);
    expect(dispatch.mock.calls.length).toBe(2);
    expect(dispatch.mock.calls[1][0]).toEqual(mSetRegion(otherRegion));
  });
});

@markerikson ๋ฌด์Šจ ๋œป์ธ๊ฐ€์š”?

mapStateToProps ๊ตฌํ˜„์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

ES6 ๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ํ†ตํ•ด ์—ฐ๊ฒฐ๋œ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋‚ด๋ณด๋‚ด๊ณ  ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ฐœ์ธ mapStateToProps์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ๋ง์”€ํ•˜์‹  ๊ฑด๊ฐ€์š” ์•„๋‹ˆ๋ฉด ๊ตฌ์ฒด์ ์œผ๋กœ ๋ง์”€ํ•ด ์ฃผ์‹ค ์ˆ˜ ์žˆ๋‚˜์š”?

@mordrax

์†์„ฑ ์ด๋ฆ„์ด ๋ชจ๋‘ ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ mapStateToProps๋ฅผ ๋…ธ์ถœํ•˜๊ณ  ๊ณต๊ฐœ๋กœ ๋งŒ๋“  ๋‹ค์Œ ๊ฐ€์งœ ์ €์žฅ์†Œ๋ฅผ ๋ณด๋‚ด๊ณ  state prop์„ ๋ณด๋‚ด๋Š” ๊ฒƒ์„ ํฌํ•จํ•˜์—ฌ ํ…Œ์ŠคํŠธ์—์„œ ContainerComponent๋ฅผ ๋ Œ๋”๋งํ•˜๋ฉด mapStateToProps๊ฐ€ ํ•ด๋‹น ์ƒํƒœ๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ  prop์— ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ ์‹œ์ ์—์„œ ์–ด๋–ป๊ฒŒ ๊ทธ๊ฒƒ์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? Connect๋Š” mapStateToProps๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์†Œํ’ˆ์„ ๋ณ‘ํ•ฉํ•˜์ง€๋งŒ ์ด์Œ๋งค๋Š” ์–ด๋””์— ์žˆ๊ณ  ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๊ตฌ์„ฑ ์š”์†Œ์— ์„ค์ •๋˜๋Š” ์†Œํ’ˆ์„ ๊ฐ€๋กœ์ฑŒ ์ˆ˜ ์žˆ๋Š” ํ•ด๋‹น ํ”„๋กœ์„ธ์Šค/ํ๋ฆ„ ์ค‘์— ์ฝ”๋“œ์˜ ์ด์Œ์ƒˆ๋Š” ์–ด๋””์ž…๋‹ˆ๊นŒ? @guatedude2 ์™€ ๊ฐ™์€ ์ด์ค‘ ์–•์Œ์€ ์ด์Œ์ƒˆ ์—†์ด ๋งคํ•‘๋œ ์†Œํ’ˆ์„ ํ™•์ธํ•˜๋Š” ํ•œ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค...

๋˜ํ•œ ๋‹น์‹ ์˜ shallow.shallow์—. ๊ทธ๋ž˜์„œ connect() ๊ตฌ์„ฑ ์š”์†Œ๋Š” ๋ž˜ํผ๋ฅผ ๋‹ค์‹œ ์ „๋‹ฌํ•ฉ๋‹ˆ๊นŒ? ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๊ฐ์‹ธ๋Š” ๋ž˜ํผ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

@granmoe ๊ฐ€ ์ •ํ™•ํžˆ ๋ฌด์—‡์„ ์˜๋ฏธํ•˜๋Š”์ง€ ์ž์„ธํžˆ ์„ค๋ช…ํ•ด ์ฃผ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

์—ฐ๊ฒฐ์— ์ „๋‹ฌํ•˜๋Š” ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ์ ์šฉ ๋ฒ”์œ„๋„ ์žˆ๋Š” ๊ฒฝ์šฐ ๊ตฌ์„ฑ ์š”์†Œ ์ „์ฒด์— ๋Œ€ํ•œ ์ „์ฒด ์ ์šฉ ๋ฒ”์œ„๊ฐ€ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‚˜๋Š” ๋˜ํ•œ @tugorez ์™€ ํ•จ๊ป˜ _๋ฌด์—‡์„_ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ๊ณ  ๊ทธ ์ด์œ ๋ฅผ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

@markerikson ์€ ์ŠคํŒŒ์ด์— ๋Œ€ํ•œ ์ข‹์€ ์˜ˆ์ž…๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ ํ•œ ๊ฐ€์ง€์ด์ง€๋งŒ "ํ–‰๋™ ๋‹จ์œ„"๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ๋” ๋งŽ์€ ๊ฒ€์ฆ์ž๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. mapDispatchToProps์—์„œ ์ŠคํŒŒ์ด ์ž‘์—…์ด ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋Š” ์ปจํ…Œ์ด๋„ˆ ๊ตฌ์„ฑ ์š”์†Œ์— ๋Œ€ํ•œ ์ „์ฒด ๋‚ด์šฉ์„ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ปจํ…Œ์ด๋„ˆ์˜ ์ฒ˜๋ฆฌ๊ธฐ ๋…ผ๋ฆฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์˜ˆ์ƒ๋˜๋Š” ๊ฒฐ๊ณผ์— ๋Œ€ํ•ด ์–ด์„ค์…˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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

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

2) ๋””์ŠคํŒจ์น˜ ํ•ธ๋“ค๋Ÿฌ ๋กœ์ง(๋™์ž‘)์ด ์ปจํ…Œ์ด๋„ˆ์—์„œ ์ž‘๋™ํ•ฉ๋‹ˆ๊นŒ?

@dschinkel :

Redux ์—ฐ๊ฒฐ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์ •์˜ํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ ๋ฐฉ๋ฒ•์€ export default connect()(MyComponent) ๋ฐ export MyComponent ๋ช…๋ช…๋œ ๋‚ด๋ณด๋‚ด๊ธฐ์ž…๋‹ˆ๋‹ค. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ mapState ๋Š” ํ•จ์ˆ˜์ผ ๋ฟ์ด๋ฏ€๋กœ $# export const mapState = () => {} ๋ฅผ ๊ฐ€์ ธ์˜จ ๋‹ค์Œ ๋ณ„๋„๋กœ ๊ฐ€์ ธ์˜ค๊ณ  ํ…Œ์ŠคํŠธํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

"์ปจํ…Œ์ด๋„ˆ ๊ตฌ์„ฑ ์š”์†Œ" ๋Œ€ "ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๊ตฌ์„ฑ ์š”์†Œ"๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒฝ์šฐ: ์—ฌ๊ธฐ์„œ ์ผ๋ฐ˜์ ์ธ ์ƒ๊ฐ์€ "์ปจํ…Œ์ด๋„ˆ" === "์ถœ๋ ฅ"์ธ ๊ฒฝ์šฐ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ปจํ…Œ์ด๋„ˆ ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•ด ๊ฑฑ์ •ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. connect ". React-Redux๋Š” ์ด๋ฏธ ๊ทธ๊ฒƒ์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•ด์•ผ ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์™„์ „ํ•œ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ ๋‹น์‹ ์ด ๊ทธ ๋…ธ๋ ฅ์„ ๋ณต์ œํ•  ์ด์œ ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ƒ์  ๊ตฌ๋…, mapState ๋ฐ mapDispatch ํ˜ธ์ถœ, ๋ž˜ํ•‘๋œ ๊ตฌ์„ฑ ์š”์†Œ์— ์†Œํ’ˆ ์ „๋‹ฌ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•œ๋‹ค๋Š” ๊ฒƒ์„ _์•Œ๊ณ _ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์†Œ๋น„์ž๋กœ์„œ _๋‹น์‹ ์ด_ ๊ด€์‹ฌ์„ ๊ฐ€์งˆ ๊ฒƒ์€ _๋‹น์‹ ์˜_ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€์ž…๋‹ˆ๋‹ค. ์ž์‹ ์˜ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ connect ๋ž˜ํผ, ํ…Œ์ŠคํŠธ ๋˜๋Š” ๋‹ค๋ฅธ ๊ฒƒ์œผ๋กœ๋ถ€ํ„ฐ props๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”์ง€ ์—ฌ๋ถ€๋Š” ์‹ค์ œ๋กœ ์ค‘์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํŠน์ • props ์ง‘ํ•ฉ์ด ์ฃผ์–ด์ง€๋ฉด ํ•ด๋‹น ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€์ž…๋‹ˆ๋‹ค.

(๋˜ํ•œ, FWIW, ๋‚˜๋Š” ๋‹น์‹ ์ด ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ์ ˆ๋Œ€์ ์œผ๋กœ ์ „๋ฌธ๊ฐ€๋ผ๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์ €๋Š” ๊ทธ๋ ‡์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‹น์‹ ์ด ์‚ฌ๋ฌผ์— ๋Œ€ํ•ด ์•ฝ๊ฐ„ ํŽธ์ง‘์ฆ์„ ๋Š๋ผ๊ณ  ์žˆ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค _does_): mapState ์— ๋Œ€ํ•ด ๊ฑฑ์ •ํ•œ๋‹ค๋ฉด ์‹ค์ˆ˜๋กœ ๊นจ์ง„ ๋‹ค์Œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ๊ณ„์† ์ง„ํ–‰ํ•˜์‹ญ์‹œ์˜ค.)

์ด์ œ ๋ž˜ํ•‘๋œ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ๋‚ด๋ถ€์— ์—ฐ๊ฒฐ๋œ ๋‹ค๋ฅธ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ  ํŠนํžˆ ์–•์€ ๋ Œ๋”๋ง ๋Œ€์‹  ์ „์ฒด ๋ Œ๋”๋ง์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ const wrapper = mount(<Provider store={testStore}><MyConnectedComponent /></Provider> ๋ฅผ ์ˆ˜ํ–‰ํ•˜์—ฌ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ๋” ๊ฐ„๋‹จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ „๋ฐ˜์ ์œผ๋กœ ๋‚ด ์ƒ๊ฐ์€ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ mapState ๊ธฐ๋Šฅ๊ณผ "์ผ๋ฐ˜" ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ณ„๋„๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ _ ์žˆ์–ด์•ผ _ ํ•ด์•ผ ํ•˜๋ฉฐ, ๋‹จ์ง€ ํ™•์ธ์„ ์œ„ํ•ด ์—ฐ๊ฒฐ๋œ ๋ฒ„์ „์„ ํ…Œ์ŠคํŠธํ•˜๋ ค๊ณ  ํ•ด์„œ๋Š” ์•ˆ ๋œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. mapState ์˜ ์ถœ๋ ฅ์ด ์ผ๋ฐ˜ ๊ตฌ์„ฑ ์š”์†Œ๋กœ ์ „๋‹ฌ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ ๋กœ Dan์ด ๋‚ด๋ถ€์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๋Š” ์ž‘์—…์„ ์„ค๋ช…ํ•˜๊ธฐ ์œ„ํ•ด ์–ผ๋งˆ ์ „์— ์ž‘์„ฑํ•œ connect ์˜ ์ถ•์†Œ ๋ฒ„์ „์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. https://gist.github.com/gaearon/1d19088790e70ac32ea636c025ba424e .

์˜ˆ, mapStateToProps ๋ฐ dispatchStateToProps๋ฅผ ๋‚ด๋ณด๋‚ด๋ฉด ๋” ์ข‹์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋‚˜๋Š” connect()(๋‚ด ํ…Œ์ŠคํŠธ์˜ ๊ณต๋™ ์ž‘์—…์ž)๊ฐ€ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ์ง€ ์•Š์Šต๋‹ˆ๋‹ค . ๊ทธ๋ ‡๊ฒŒ ํ•˜๋Š” ํ•œ ๊ฐ€์ง€ ๋ถ„๋ช…ํ•œ ๋ฐฉ๋ฒ•์ด ๋  ๊ฒƒ์ด๋ฏ€๋กœ ๋‹ค์Œ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค.

example-container-spec.js

describe('testing mapPropsToState directly', () => {
    it.only('returns expected state', () => {
        const state = {firstName: 'Dave'};
        const output = mapStateToProps(state);

        expect(output.firstName).to.equal('Dave');
    });
});

์ปจํ…Œ์ด๋„ˆ

import React from 'react';
import { connect } from 'react-redux'
import Example from './Example';

export const mapStateToProps = (state) => ({
    firstName: state.firstName
})

export default connect(mapStateToProps)(Example);

ํ”„๋ ˆ์  ํ…Œ์ด์…˜

import React, { Component } from 'react';

export default class Example extends Component {
    render(){
        return (
            <div className='example'>
                {this.props.firstName}
            </div>
        )
    }
}

๊ทธ๋Ÿฐ ๋‹ค์Œ ๋‹ค์‹œ _shallow_ ๋ Œ๋”๋ง์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด์™€ ๊ฐ™์€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์—ฌ์ „ํžˆ ํ•˜์œ„ ๊ตฌ์„ฑ ์š”์†Œ๋กœ ๋‹ค์ด๋น™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

example-container-spec.js

it('persists firstName to presentation component', () => {
        var state = {firstName: "Dave"};
        const container = mount(<ExampleContainer store={fakeStore(state)}/>),
            example = container.find(Example);

        expect(example.length).to.equal(1);
        expect(example.text()).to.equal(state.firstName);
    });

์ปจํ…Œ์ด๋„ˆ

import React from 'react';
import { connect } from 'react-redux'
import Example from './Example';

const mapStateToProps = (state) => ({
    name: state.firstName
})

export default connect(mapStateToProps)(Example);

ํ”„๋ ˆ์  ํ…Œ์ด์…˜

import React, { Component } from 'react';

export default class Example extends Component {
    render(){
        return (
            <div className='example'>
                {this.props.firstName}
            </div>
        )
    }
}

์ด๊ฒƒ์„ ์–•๊ฒŒ ํ•˜๋Š” ๋˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์€ ์ด์ค‘ ์–•๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์–•์€ ๋ถ€๋ชจ์˜ ์–•์€ ๊ฒƒ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ž˜ํ•‘๋˜๋Š” ์ž์‹ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์–•์€ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

it('get child component by double shallow()', () => {
        const container = shallow(<ExampleContainer store={fakeStore({})}/>),
            presentationalChildComponent = container.find(Example).shallow().find('.example');

        expect(presentationalChildComponent).to.have.length(1);
    });

๋”ฐ๋ผ์„œ ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๊ตฌ์„ฑ ์š”์†Œ์˜ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋Š” ๋˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

์–ด๋Š ์ชฝ์ด๋“  ์ž‘๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ prop ์ƒํƒœ ์„ค์ • ์ธก๋ฉด์—์„œ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐ ๊ด€์‹ฌ์ด ์žˆ๋Š” ๊ฒƒ์€ mapStateToProps๋ฅผ ๋‚ด๋ณด๋‚ด๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹์Šต๋‹ˆ๋‹ค.

๋‚˜๋Š” ๋‹น์‹ ์ด ํ…Œ์ŠคํŠธ์˜ ์ ˆ๋Œ€์ ์œผ๋กœ ์ „๋ฌธ๊ฐ€์ด๊ณ  ๋‚˜๋Š” ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ์ง€๋งŒ, ๋‹น์‹ ์ด ์‚ฌ๋ฌผ์— ๋Œ€ํ•ด ์•ฝ๊ฐ„ ํŽธ์ง‘์ฆ์„ ๋Š๋ผ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค :)

์•„๋‹™๋‹ˆ๋‹ค. ์˜ฌ๋ฐ”๋ฅธ ํ…Œ์ŠคํŠธ๋Š” ๊นจ์ง€์ง€ ์•Š๊ณ  ๊ฐ€์น˜๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ข‹์€ ํ…Œ์ŠคํŠธ๋ฅผ ๊ฐ–๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์šฐ์„  ์•„๋ฌด๋„ _์ „๋ฌธ๊ฐ€_๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ๋ชจ๋“  ์‚ฌ๋žŒ์€ ๊ณ„์†ํ•ด์„œ ๋ฐฐ์šฐ๊ณ  ์žˆ์œผ๋ฉฐ ์ผ๋ถ€๋Š” ์ž์กด์‹ฌ ๋•Œ๋ฌธ์— ๊ทธ๊ฒƒ์„ ์ธ์ •ํ•˜๊ณ  ์‹ถ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์†Œํ”„ํŠธ์›จ์–ด ์žฅ์ธ ์ •์‹ (์ผ๋ช… ์ „๋ฌธ์„ฑ)์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๋‹น์‹ ์ด ๋งํ–ˆ๋“ฏ์ด ํ–‰๋™ ๋งŒ ํ…Œ์ŠคํŠธํ•˜๊ณ  ํ˜‘๋ ฅ์ž (connect() ์ž์ฒด)๋ฅผ ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ฒ˜์Œ์œผ๋กœ ๊ทธ๊ฒƒ์„ ํŒŒ์•…ํ•˜๊ณ  ์ž˜ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ณต๋™ ์ž‘์—…์ž๋ฅผ ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋ฉฐ ๋•Œ๋กœ๋Š” ๋™๋ฃŒ ๊ฐœ๋ฐœ์ž ๊ฐ„์˜ ํ† ๋ก ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์ทจ์„ฑ ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ทจ์„ฑ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋Š” ์ง„์ง€ํ•˜๊ฒŒ ๋ฐ›์•„๋“ค์—ฌ์•ผ ํ•˜๋ฉฐ, ์ด๋Š” ํ…Œ์ŠคํŠธ์— ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ‘๊ทผํ•˜๊ณ  ์žˆ๋Š”์ง€ ์ง€์†์ ์œผ๋กœ ํ™•์ธํ•ด์•ผ ํ•จ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ๋‚˜๋Š” ๋‹จ์ง€ ๋‚ด ํ๋ฆ„์„ ํŒŒ์•…ํ•˜๋ ค๊ณ  ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. I TDD, ๊ทธ๋ž˜์„œ ์‹œ์ž‘ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์ค‘์š”ํ•˜๊ณ  ์ข‹์€ ํ…Œ์ŠคํŠธ๊ฐ€ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์•„๋ฌด๋„ React ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์„ ์ดํ•ดํ•˜๋ ค๊ณ  ํ•  ๋•Œ "ํŽธ์ง‘์ฆ"์ด๋ผ๊ณ  ๋Š๋ผ์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด React ํ…Œ์ŠคํŠธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ถœ์‹œ ์ค‘...
์‹ค์ œ๋กœ react-redux ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ๋‹ค์–‘ํ•œ ์Šคํƒ€์ผ๊ณผ ์กฐํ•ฉ ๋ฐ ์ ‘๊ทผ ๋ฐฉ์‹์„ ๋ณด์—ฌ์ฃผ๋Š” ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ๊ณง ๊ณต์œ ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค. ๋‚˜๋Š” ์ด๊ฒƒ์— ๋Œ€ํ•ด ์ƒ๊ฐํ•˜๋Š” ์œ ์ผํ•œ ์‚ฌ๋žŒ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ๊ฒƒ์ด ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ๋„์›€์ด ๋  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ๋žŒ๋“ค์ด redux ์ปจํ…Œ์ด๋„ˆ ๋ฐ ๊ธฐํƒ€ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ๊ณผ ๊ด€๋ จํ•˜์—ฌ ๋™์ผํ•œ ์งˆ๋ฌธ์„ ํ•˜๋Š” ๊ฒƒ์„ ๋ช‡ ๋ฒˆ์ด๊ณ  ๋ณด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. Redux ๋ฌธ์„œ๋‚˜ react-redux ๋ฌธ์„œ๋Š” ์ด์— ๋Œ€ํ•ด ์ •์˜๋ฅผ ๋‚ด๋ฆฌ์ง€ ์•Š์œผ๋ฉฐ IMO ํ…Œ์ŠคํŠธ ์˜์—ญ์— ๋ฌธ์„œ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์€ ๋‹จ์ง€ ํ‘œ๋ฉด์„ ๊ธ๋Š” ๊ฒƒ์ผ ๋ฟ์ด๋ฉฐ React ํ…Œ์ŠคํŠธ์— ์ ‘๊ทผํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๋‹ค์–‘ํ•œ ์Šคํƒ€์ผ์„ ๋ณด์—ฌ์ฃผ๋Š” ๋ฉ‹์ง„ ์ €์žฅ์†Œ๊ฐ€ ํ•„์š”ํ•˜๋ฏ€๋กœ ๊ณง ์˜ฌ๋ฆฌ๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

If anyone would like to contribute to that repo, please get a hold of me ๊ท€ํ•˜๋ฅผ ๊ณต๋™ ์ž‘์—…์ž๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ๋žŒ๋“ค์ด ์˜ˆ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์œผ๋ฉด ํ•ฉ๋‹ˆ๋‹ค. ๋‚˜๋Š” Enzyme , Ava , Jest , tape ๋“ฑ์ด ํฌํ•จ๋œ React Test Utils + mocha ๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์ œ๋ฅผ ๋ณด๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก  :

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

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

@markerikson ์ž…๋ ฅํ•ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•˜๋Š” ๊ฒƒ์„ ์ข‹์•„ํ•ฉ๋‹ˆ๋‹ค. ์ข‹์€ ๋ฌผ๊ฑด ๋‚จ์ž. ๋‚˜๋Š” ๋‹น์‹ ์˜ ์ „๋ฌธ ์ง€์‹์— ์˜๋ฌธ์„ ์ œ๊ธฐํ•œ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์‹ค์ œ๋กœ ์ง์ ‘ mapStateToProps ํ…Œ์ŠคํŠธ๋ฅผ ์‹œ๋„ํ•˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค! ํ…Œ์Šคํ„ฐ๊ฐ€ ์•„๋‹Œ ๊ฒƒ์ด ๋‚˜์œ ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค ;). ์ €๋Š” ๋‹จ์ˆœํžˆ redux๋ฅผ ์ฒ˜์Œ ์ ‘ํ–ˆ์„ ๋ฟ์ด๋ฏ€๋กœ "ํ‘œ๋ฉด์— ๋‹ฟ๋Š” ๊ฒƒ"์ด โ€‹โ€‹์•„๋‹Œ ์งˆ๋ฌธ์ด ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์‚ฌ๋žŒ๋“ค์ด ์ž์‹ ์ด ํ•˜๋Š” ์ผ, ์ˆ˜ํ–‰ ๋ฐฉ๋ฒ•, ์ ‘๊ทผ ๋ฐฉ์‹์˜ ์ด์ ์„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋„๋ก ๋‚ฎ์€ ์ˆ˜์ค€์—์„œ ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•ด ๋…ผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ ค๋ฉด "๋™์ž‘๋งŒ ํ…Œ์ŠคํŠธ"ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๋‚ฎ์€ ์ˆ˜์ค€์˜ connect, react-redux, provider๋ฅผ ์ดํ•ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‚˜์—๊ฒŒ๋„ ํฅ๋ฏธ๋กœ์šด mapStateToProps๋ฅผ ๋‚ด๋ณด๋‚ด๋Š” ์‚ฌ๋žŒ๋“ค์„ ๋งŽ์ด ๋ณด์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์™œ ์•ˆ ๋˜๋Š”์ง€ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค.

WeDoTDD.com
๊ด€์‹ฌ ์žˆ๋Š” ๋ถ„๋“ค์„ ์œ„ํ•ด ์ž‘๋…„์— WeDoTDD.com ์„ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค(React btw๋กœ ์ž‘์„ฑ). ํŠน์ • ํŒ€(ํŠน์ • ํŒ€์˜ ๋ชจ๋“  ๊ฐœ๋ฐœ์ž) TDD ๋˜๋Š” ํšŒ์‚ฌ ์ „์ฒด(์ผ๋ถ€ ํšŒ์‚ฌ์—๋Š” ๋ชจ๋“  TDD, ํŠนํžˆ ์ปจ์„คํŒ… ํšŒ์‚ฌ์— ๊ฐœ๋ฐœ์ž๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ)๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ slack.wedotdd.com ์—์„œ ์ €์—๊ฒŒ ์—ฐ๋ฝํ•˜์‹ญ์‹œ์˜ค.

์—…๋ฐ์ดํŠธ.

์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋” ๋งŽ์ด ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋‚˜์—๊ฒŒ ์ž˜ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ์ด ๋ฌด์—‡์ธ์ง€ ๋ฐ˜์˜ํ•œ ํ›„ ์—ฌ๊ธฐ @markerikson ์˜ ์ง„์ˆ ์— 100% ๋™์˜ํ•ฉ๋‹ˆ๋‹ค.

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

๋‚˜๋Š” ๋‚ด ํ…Œ์ŠคํŠธ์— <Provider /> ๋ฅผ ๋„์ž…ํ•  ํ•„์š”๋ฅผ ๋ณด์ง€ ๋ชปํ–ˆ๊ณ  <Provider /> ๊ฐ€ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด ์š”์ ์ด ์•„๋‹ ๋•Œ ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ณต๋™ ์ž‘์—…์ž๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค. ๊ตฌ์„ฑ ์š”์†Œ์˜ ๋™์ž‘์ด ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•˜๋ฉฐ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ปจํ…์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์ €์žฅ์†Œ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”์ง€ ์—ฌ๋ถ€๋Š” ์ค‘์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ƒ์ ์„ ์†Œํ’ˆ์œผ๋กœ ์ „๋‹ฌํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ์€ ์ƒ์ ์— ๋„๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ ์–ด๋–ค ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ์—์„œ ๊ณต๊ธ‰์ž๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ๋ฒ•(์ปจํ…์ŠคํŠธ ๋˜๋Š” ์†Œํ’ˆ)์€ ์ƒ๊ด€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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

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

์ผ์ด ์ž˜ ํ’€๋ ธ๋‹ค๋‹ˆ ๋‹คํ–‰์ž…๋‹ˆ๋‹ค. (FWIW, ๋‚ด ์˜๊ฒฌ์€ "์ด์Œ์ƒˆ" ๋ฐ "์†Œ๋„๊ตฌ ํŒŒ๊ณ ๋“ค๊ธฐ"์— ๋Œ€ํ•œ ๊ท€ํ•˜์˜ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๊ฑฐ์˜ ๋˜๋Š” ์ „ํ˜€ ๋„์›€์ด ๋˜์ง€ ์•Š๋Š” ๋ถˆํ•„์š”ํ•œ ์„ธ๋ถ€ ์ˆ˜์ค€์œผ๋กœ ๊ฐ€๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์˜€์Šต๋‹ˆ๋‹ค.)

์—ฐ๊ฒฐ๋œ ๋‹ค๋ฅธ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๊ตฌ์„ฑ ์š”์†Œ์˜ ์ „์ฒด ๋ Œ๋”๋ง์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ <Provider> /context _is_ ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ํ•„์š”ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ๋œ ์ž์‹์ด ์ปจํ…์ŠคํŠธ์—์„œ ์ €์žฅ์†Œ๋ฅผ ์ฐพ์„ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ์—์„œ Redux์˜ ํ˜„์žฌ ๋ฌธ์„œ๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•œ ์ œ์•ˆ์ด ์žˆ์œผ๋ฉด ๋ฐ˜๋“œ์‹œ PR์„ ์ œ์ถœํ•˜์‹ญ์‹œ์˜ค.

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

์•„๋ฌดํŠผ ์ข‹์€ ์ž๋ฃŒ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

๋งž์Šต๋‹ˆ๋‹ค. mount ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๋‹น ๊ตฌ์„ฑ ์š”์†Œ์˜ ์ˆ˜๋ช… ์ฃผ๊ธฐ๋ฅผ ํ™•์ธํ•˜๊ณ  ํ•ด๋‹น ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์—ฐ๊ฒฐ๋œ ๋‹ค๋ฅธ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ ์˜ค๋ฅ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ค‘์ฒฉ๋œ ๊ตฌ์„ฑ ์š”์†Œ์— ๋Œ€ํ•œ ์ปจํ…์ŠคํŠธ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ €์žฅ์†Œ๊ฐ€ _ํ•„์š”_ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ค‘์ธ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

(ํŽธ์ง‘: ๋‚˜๋Š” ๋ถ„๋ช…ํžˆ ๋‚˜ ์ž์‹ ์„ ๋ฐ˜๋ณตํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ•ญ์ƒ ์Šค๋ ˆ๋“œ๋ฅผ ํ†ตํ•ด ๋Œ์•„๊ฐ€์ง€ ์•Š๊ณ  ์‹ฌ์ง€์–ด ์•ฝ๊ฐ„ ์œ„๋กœ ์Šคํฌ๋กคํ•˜์ง€ ์•Š๊ณ ๋„ ์—ฌ๋Ÿฌ ์žฅ์†Œ์—์„œ ์งˆ๋ฌธ์— ๋Œ€๋‹ตํ•˜๋Š” ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.)

์•„ ์•Œ์•˜์–ด! ๋„ค ๊ทธ๊ฑด ์ƒ๊ฐ์„ ๋ชปํ–ˆ๋„ค์š”...

@markerikson ๋ฐ @dschinkel์˜ ๋ชจ๋“  ์˜๊ฒฌ์— ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค! ๊ทธ๋“ค์€ ๋งค์šฐ ๋„์›€์ด๋ฉ๋‹ˆ๋‹ค. ์ด์ œ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์€ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋‚ด๋ณด๋‚ด๊ณ  ๋‹ค๋ฅธ ๊ตฌ์„ฑ ์š”์†Œ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ  mapStateToProps ๋ฐ mapDispatchToProps ๋„ ๋ณ„๋„๋กœ ํ…Œ์ŠคํŠธํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋Ÿฌํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ํฌ์ฐฉํ•˜์ง€ ๋ชปํ•˜๋Š” ํ•œ ๊ฐ€์ง€ ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋Š”๋ฐ, map(State/Dispatch)ToProps ๊ฐ€ ์‹ค์ œ๋กœ connect ์— ๋Œ€ํ•œ ํ˜ธ์ถœ๋กœ ์ „๋‹ฌ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค. ์˜ˆ์‹œ:

import React from 'react';
import { connect } from 'react-redux';

export const mapStateToProps = ({ name }) => ({ name });

export const Example = ({ name }) => <h1>{name}</h1>;

export default connect({ name } => ({ name: firstName }))(Example); // Whoops, didn't actually pass in `mapStateToProps`.

์ด๊ฒƒ์€ ์•„๋งˆ๋„ ์‹ค์ œ๋กœ ๋ฐœ์ƒํ•˜์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ํŠนํžˆ linters๊ฐ€ mapStateToProps ๋ฅผ ๋‚ด๋ณด๋‚ด๋Š” ๋™์•ˆ ์‚ฌ์šฉํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋กœ ๋ณด๊ณ ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@danny-andrews mapDispatchToProps ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๋” ๊ตฌ์ฒด์ ์œผ๋กœ ๋ง์”€ํ•ด ์ฃผ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

@leizard

๋” ์ด์ƒ mapDispatchToProps ๋˜๋Š” mapStateToProps๋ฅผ ์ง์ ‘ ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด ์Šค๋ ˆ๋“œ ์ดํ›„๋กœ ๋งˆ์Œ์ด ๋ฐ”๋€Œ์—ˆ๊ณ  ๊ทธ๋ ‡๊ฒŒ ํ•˜์ง€ ๋ง๋ผ๊ณ  ์กฐ์–ธํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ ํ…Œ์ŠคํŠธํ•˜๋ฉด @danny-andrews ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€๋งŒ ๋ฌธ์ œ๋Š” ์ข‹์€ ํ…Œ์ŠคํŠธ์— ๊ด€ํ•œ ๊ฒƒ์ด๋ฉฐ ์ด ๋‘ ๊ธฐ๋Šฅ์„ ๋…ธ์ถœํ•˜๋ ค๋Š” ๊ฒƒ์€ ์ข‹์€ ์ƒ๊ฐ์ด ์•„๋‹™๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚ฌ์Šต๋‹ˆ๋‹ค. ๋™์ž‘์„ ํ…Œ์ŠคํŠธํ•˜๊ฑฐ๋‚˜ ์–•์€ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํ†ตํ•ด ์†Œํ’ˆ์„ ํ…Œ์ŠคํŠธํ•˜์—ฌ ์ด ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ๊ฐ„์ ‘์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‚˜๋Š” ๊ทธ๋Ÿฌํ•œ ๊ฐœ์ธ ๋ฉ”์„œ๋“œ๋ฅผ ๋…ธ์ถœํ•˜๋ ค๊ณ  ํ•  ์ด์œ ๊ฐ€ ์—†๋‹ค๋Š” ๊ฒƒ์„ ์•Œ์•˜๊ณ  ์ด์ œ ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜๋Š” ๊ฒƒ๋„ ๋‚˜์œ ํ…Œ์ŠคํŠธ ๊ด€ํ–‰์ด๋ผ๋Š” ๊ฒƒ์„ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋‚˜๋Š” @markerikson ์— ๋™์˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํ†ตํ•ด ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค.

์˜ค๋ฒ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์•„๋‹Œ ํ…Œ์ŠคํŠธ์— ์ ํ•ฉํ•œ API/๊ณ„์•ฝ์„ ์ฐพ๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. :).

@dschinkel

๋™์ž‘์„ ํ…Œ์ŠคํŠธํ•˜์—ฌ ์ด ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ๊ฐ„์ ‘์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค...
๊ทธ๋ž˜์„œ ๋‚˜๋Š” @markerikson ์— ๋™์˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํ†ตํ•ด ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค.

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

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

  1. ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์€ ๊ตฌ์„ฑ ์š”์†Œ ๋ฐ ์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ ๋‚ด๋ณด๋‚ด๊ธฐ
  2. mapStateToProps ๋˜๋Š” mapDispatchToProps ์ˆ˜์ถœ ๊ธˆ์ง€
  3. ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์€ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํ†ตํ•ด ๋ชจ๋“  ๊ตฌ์„ฑ ์š”์†Œ ๋…ผ๋ฆฌ ํ…Œ์ŠคํŠธ
  4. redux์™€์˜ ์ƒํ˜ธ ์ž‘์šฉ์„ ํ…Œ์ŠคํŠธํ•  ๋•Œ ์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ๋งŒ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค(store/action props์˜ ์ ์ ˆํ•œ ์œ„์น˜์—์„œ ๋ฐ์ดํ„ฐ props๊ฐ€ ์ ์ ˆํ•œ action ์ž‘์„ฑ์ž์—๊ฒŒ ํ• ๋‹น๋จ).

4๋ฒˆ์„ ์ˆ˜ํ–‰ํ•  ๋•Œ์˜ ์œ ์ผํ•œ ์ถ”๊ฐ€ ์˜ค๋ฒ„ํ—ค๋“œ๋Š” ์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ์— ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด redux ์ €์žฅ์†Œ๋ฅผ ์ œ๊ฑฐํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ API๊ฐ€ ์„ธ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์— ๋ถˆ๊ณผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฒƒ์€ ๋งค์šฐ ์‰ฝ์Šต๋‹ˆ๋‹ค.

MapStateToProps ๋ฉ”์„œ๋“œ

TestContainer.jsx:

import { connect } from 'react-redux';

export const TestContainer = ({ permissions }) => (permissions['view-message'] ? 'Hello!' : null);

export const mapStateToProps = ({ auth }) => ({ permissions: auth.userPermissions });

export default connect(mapStateToProps)(TestContainer); // Ewww, exposing private methods just for testing

TestContainer.spec.jsx:

import React from 'react';
import { shallow } from 'enzyme';

import TestContainer, { mapStateToProps } from './TestContainer';

it('maps auth.userPermissions to permissions', () => {
  const userPermissions = {};

  const { permissions: actual } = mapStateToProps({
    auth: { userPermissions },
  });

  expect(actual).toBe(userPermissions);
});

์ƒˆ๋กœ์šด ๋ฐฉ๋ฒ•

TestContainer.jsx:

import { connect } from 'react-redux';

export const TestContainer = ({ permissions }) => (permissions['view-message'] ? 'Hello!' : null);

const mapStateToProps = ({ auth }) => ({ permissions: auth.userPermissions });

export default connect(mapStateToProps)(TestContainer);

TestContainer.spec.jsx:

import React from 'react';
import { shallow } from 'enzyme';

import TestContainer, { TestComponent } from './TestContainer';

it('renders TestComponent with approriate props from store', () => {
  const userPermissions = {};
  // Stubbing out store. Would normally use a helper method. Did it inline for simplicity.
  const store = {
    getState: () => ({
      auth: { userPermissions },
    }),
    dispatch: () => {},
    subscribe: () => {},
  };
  const subject = shallow(<TestContainer store={store} />).find(TestComponent);

  const actual = subject.prop('permissions');
  expect(actual).toBe(userPermissions);
});

์˜ˆ, mapStateToProps๋ฅผ ๋‚ด๋ณด๋‚ด๊ณ  ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š๊ณ  ํ…Œ์ŠคํŠธ ๊ฐ€ ๋๋‚ฌ์Šต๋‹ˆ๋‹ค. API ๋ฅผ ํ†ตํ•ด ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. API๋Š” Container ์ž…๋‹ˆ๋‹ค.

@danny-andrews

์—ฐ๊ฒฐ๋˜์ง€ ์•Š์€ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํ†ตํ•ด ๋ชจ๋“  ๊ตฌ์„ฑ ์š”์†Œ ๋…ผ๋ฆฌ ํ…Œ์ŠคํŠธ

๋ช‡ ๊ฐ€์ง€ ์˜ˆ๋ฅผ ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

๋‚˜๋Š” ๋‹น์‹ ์ด ๋งํ•œ ๊ฒƒ๊ณผ ๊ฐ™์€ ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. "ํ–‰๋™์„ ํ…Œ์ŠคํŠธํ•˜์—ฌ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ๊ฐ„์ ‘์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค."

์šฉ์–ด๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๋Š” ๊ฒƒ์ด ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "์—ฐ๊ฒฐ๋˜์ง€ ์•Š์€ ๊ตฌ์„ฑ ์š”์†Œ"๋ผ๊ณ  ํ•  ๋•Œ connect ์— ๋Œ€ํ•œ ํ˜ธ์ถœ์— ๋ž˜ํ•‘๋œ ์›์‹œ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์˜๋ฏธํ•˜๊ณ  "์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ"๋ผ๊ณ  ๋งํ•  ๋•Œ๋Š” ๋‹ค์Œ์—์„œ ๋ฐ˜ํ™˜๋˜๋Š” ๊ฒฐ๊ณผ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. connect ์— ๋Œ€ํ•œ ํ˜ธ์ถœ "์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ"์™€ "์ปจํ…Œ์ด๋„ˆ"๋Š” ๋™์˜์–ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๋‚ด ์œ„์—์„œ :

// Unconnected component. Test all component logic by importing this guy.
export const TestComponent = ({ permissions }) => (permissions['view-message'] ? 'Hello!' : null);

const mapStateToProps = ({ auth }) => ({ permissions: auth.userPermissions });

// Connected component (container). Only test interaction with redux by importing this guy.
export default connect(mapStateToProps)(TestComponent);

์–ด๋–ค ์‚ฌ๋žŒ๋“ค์€ ์ด๊ฒƒ๋“ค์„ ์™„์ „ํžˆ ๋ถ„ํ• ํ•˜๊ณ  "์ปจํ…Œ์ด๋„ˆ"๊ฐ€ ์ˆœ์ „ํžˆ ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ž˜ํ•‘ํ•˜๋Š” connect ์— ๋Œ€ํ•œ ํ˜ธ์ถœ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ์„ ํ˜ธํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ๋ชฉ๋ก์˜ 1๋ฒˆ๊ณผ 3๋ฒˆ์€ ์‚ฌ๋ผ์ง€๊ณ  ์ž‘์„ฑํ•ด์•ผ ํ•˜๋Š” ํ…Œ์ŠคํŠธ๋Š” redux(4๋ฒˆ)์™€์˜ ์ƒํ˜ธ ์ž‘์šฉ์„ ํ™•์ธํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฟ์ž…๋‹ˆ๋‹ค.

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

์ด ์Šค๋ ˆ๋“œ๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฝ์€ ํ›„ ๊ถŒ์žฅ๋˜๋Š” 3๊ฐ€์ง€ ์ ‘๊ทผ ๋ฐฉ์‹์ด ์žˆ๋‹ค๋Š” ๊ฒฐ๋ก ์— ๋„๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. mapDispatchToProps ๋ฐ mapStateToProps ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฐ ๋ณ„๋„๋กœ ํ…Œ์ŠคํŠธ
  2. ์ปจํ…Œ์ด๋„ˆ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์–•๊ฒŒ ๋ Œ๋”๋งํ•˜๊ณ  ๋ฐฐ์„ ์ด ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.
  3. mapStateToProps ๋Œ€์‹  ์„ ํƒ์ž๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  mapDispatchToProps ๋Œ€์‹  ์•ก์…˜ ์ž‘์„ฑ์ž๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๋ณ„๋„๋กœ ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค. ์ „์ฒด ํ๋ฆ„์ด ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ž‘์—… ์ƒ์„ฑ์ž, ๊ฐ์†๊ธฐ ๋ฐ ์„ ํƒ๊ธฐ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

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

@ucorina
ES6 ๋ชจ๋“ˆ์˜ ํ˜„์žฌ ์„ธ๊ณ„์—์„œ ๊ฐ ํŒŒ์ผ์€ ์ž์ฒด ๋ฒ”์œ„์— ๋ž˜ํ•‘๋œ ๋ชจ๋“ˆ๋กœ ๊ฐ„์ฃผ๋ฉ๋‹ˆ๋‹ค. ๋‚ด๋ณด๋‚ด๊ธฐ๋Š” ์ž‘์€ ๋ชจ๋“ˆ์˜ API์ด๋ฉฐ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ฉ”์„œ๋“œ๋ฅผ ๋‚ด๋ณด๋‚ด๋Š” ๊ฒƒ์€ ๋งค์šฐ ์ž˜๋ชป๋œ ์ผ์ž…๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋น„๊ณต๊ฐœ ๋ฉ”์„œ๋“œ๋ฅผ ๊ณต๊ฐœํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๋น„์Šทํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์ด ๋‚ด๊ฐ€ @dschinkel ์ ‘๊ทผ ๋ฐฉ์‹์„ ์„ ํ˜ธํ•˜๊ณ  mapDispatch ๋ฐ mapState๋ฅผ ๋‚ด๋ณด๋‚ด์ง€ ์•Š๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๊ทธ๋“ค์€ ์—ฐ๊ฒฐ๋œ ๊ตฌ์„ฑ ์š”์†Œ์˜ ๋น„๊ณต๊ฐœ ๊ตฌํ˜„์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์—ฐ๊ฒฐ ์ฃผ์œ„์— ๋‹ค๋ฅธ ๊ตฌ์„ฑ ์š”์†Œ๋งŒ ๋ž˜ํ•‘ํ•˜๋Š” ๊ตฌ์„ฑ ์š”์†Œ๋กœ ๋ฌด์—‡์„ ํ•ด์•ผ ํ• ์ง€ ์ž˜ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค.

๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๋ฌผ์„ ์ž‘์„ฑํ•œ ์ ์ด ์žˆ์Šต๋‹ˆ๊นŒ? @dschinkel ์€ ๊ทธ๊ฒƒ์„ ์ฝ๊ณ  ์‹ถ์–ดํ•ฉ๋‹ˆ๋‹ค.

@AnaRobynn

์˜ˆ, mapDispatch ๋ฐ mapState ๋ฅผ ๋‚ด๋ณด๋‚ด๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ ์ž˜๋ชป๋œ ๋Š๋‚Œ์ด ๋“ ๋‹ค๋Š” ๋ฐ ๋™์˜ํ•ฉ๋‹ˆ๋‹ค. ์ง์ ‘ ์‚ฌ์šฉํ•˜์ง€๋Š” ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋œ "์ˆœ์ˆ˜ํ•œ" ์˜ต์…˜์ด๋”๋ผ๋„ ์œ ํšจํ•œ ์˜ต์…˜์ž…๋‹ˆ๋‹ค. . Dan Abramov๊ฐ€ https://github.com/reduxjs/react-redux/issues/325#issuecomment -199449298์—์„œ ์ด ์ •ํ™•ํ•œ ๊ธฐ์ˆ ์„ ์ œ์•ˆํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋Šฅํ•œ ์˜ต์…˜์œผ๋กœ ๋‚จ๊ฒจ ๋‘์—ˆ์Šต๋‹ˆ๋‹ค.

"์—ฐ๊ฒฐ ์ฃผ์œ„์— ๋‹ค๋ฅธ ๊ตฌ์„ฑ ์š”์†Œ๋งŒ ๋ž˜ํ•‘ํ•˜๋Š” ๊ตฌ์„ฑ ์š”์†Œ๋กœ ๋ฌด์—‡์„ ํ•  ๊ฒƒ์ธ๊ฐ€"์— ๋Œ€ํ•œ ๊ท€ํ•˜์˜ ์งˆ๋ฌธ์— ๋Œ€๋‹ตํ•˜๋ ค๋ฉด ํ…Œ์ŠคํŠธํ•˜์ง€ ๋ง๋ผ๊ณ  ๋งํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ๊ด€๋ จ๋œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์—†๊ณ  connect ๊ตฌํ˜„์„ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ด๋ฏธ react-redux ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ํ…Œ์ŠคํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜๋„ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ณ  ์‹ถ์œผ์‹œ๋‹ค๋ฉด ๋‚˜์ค‘์— ์ฝ”๋“œ๋ฅผ ์ฆ๋ช…ํ•˜๊ธฐ ์œ„ํ•ด @dschinkel ์ด ์„ค๋ช…ํ•˜๋Š” ๋‚ด์šฉ์— ๋” ๊ฐ€๊นŒ์šด ์—ฌ๊ธฐ์—์„œ ์„ค๋ช…ํ•˜๋Š” ๋‘ ๋ฒˆ์งธ ์ ‘๊ทผ ๋ฐฉ์‹์„ ํ•ญ์ƒ ๋”ฐ๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@ucorina ๋งž์•„์š”, ์˜ต์…˜๋„ ์ œ๊ฐ€ ๊ฐ€๊ณ  ์‹ถ์€ ๊ฒƒ ๊ฐ™์•„์š”! ๋‹ต๋ณ€์„ ๋” ์ž์„ธํžˆ ์„ค๋ช…ํ•ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์ž˜ ์“ด ๋ธ”๋กœ๊ทธ ๊ธ€์ž…๋‹ˆ๋‹ค. ์ถ•ํ•˜ํ•ด!

@ucorina ๋‹ค์‹œ ๊ท€์ฐฎ๊ฒŒ ํ•ด์„œ ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ž ์‹œ ๋™์•ˆ ์ž๊ณ  ๋‚˜๋ฉด ์˜ต์…˜ 2์—๋„ ์™„์ „ํžˆ ๋™์˜ํ•˜๋Š”์ง€ ํ™•์‹ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ™•์‹คํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‚˜๋Š” ๋˜ํ•œ mapStateToProps๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์˜ ์ด์ ์— ๋Œ€ํ•ด ์ „ํ˜€ ํ™•์‹ ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

'์˜ต์…˜ 2'๊ฐ€ ์•ก์…˜ ์ œ์ž‘์ž๋ฅผ ๊ฐ„์ ‘์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ๋„ ๊ดœ์ฐฎ๋‹ค๊ณ  ๋ณด์‹ญ๋‹ˆ๊นŒ? ๋˜ํ•œ ๊ฐ„์ ‘์ ์œผ๋กœ ์„ ํƒ๊ธฐ๋ฅผ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๊นŒ? ์ข‹์€ ์ผ์ด์•ผ?
๋‚˜๋Š” ๊ทธ๊ฒƒ์— ๋Œ€ํ•ด 100% ํ™•์‹ ํ•  ์ˆ˜๋Š” ์—†์ง€๋งŒ ๊ฑฐ๊ธฐ์— ์žˆ๋Š” ๋ชจ๋“  (์ž˜๋ชป๋œ) ์ •๋ณด์— ๋Œ€ํ•ด์„œ๋„ ํ˜ผ๋ž€์Šค๋Ÿฝ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ๋‹ค์–‘ํ•œ ์•„์ด๋””์–ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. mapStateToProps ๋‚ด๋ถ€์˜ ์„ ํƒ๊ธฐ๋งŒ ์‚ฌ์šฉ

    • ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ปจํ…Œ์ด๋„ˆ ํ…Œ์ŠคํŠธ ์ค‘์— ์กฐ๋กฑํ•˜์‹ญ์‹œ์˜ค.

    • ์„ ํƒ๊ธฐ๋ฅผ ๋ณ„๋„๋กœ ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค.

    • mapDispatchToProps ๋‚ด์—์„œ ์•ก์…˜ ์ƒ์„ฑ์ž๋งŒ ์‚ฌ์šฉ

    • ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ปจํ…Œ์ด๋„ˆ ํ…Œ์ŠคํŠธ ์ค‘์— ์กฐ๋กฑํ•˜์‹ญ์‹œ์˜ค.

    • ์ž‘์—… ์ƒ์„ฑ์ž๋ฅผ ๋ณ„๋„๋กœ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.

    • ์–•์€ ์—ฐ๊ฒฐ ๊ตฌ์„ฑ ์š”์†Œ

    • ๋ Œ๋”๋ง๋˜์—ˆ๋‹ค๊ณ  ์ฃผ์žฅ

    • mapStateToProps , mapDispatchToProps ๋˜๋Š” mergeProps ์— ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๊ณ  ๋‹ค๋ฅธ ๊ณณ์—์„œ๋Š” ์•„์ง ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š์€ ์ถ”๊ฐ€ ๋…ผ๋ฆฌ๋ฅผ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. (์กฐ๊ฑด๋ถ€ ๋””์ŠคํŒจ์น˜, ownProps ์—์„œ ์ธ์ˆ˜๋ฅผ ๋ฐ›๋Š” ์„ ํƒ๊ธฐ ๋“ฑ)



      • ์ด ๊ฒฝ์šฐ ์˜ฌ๋ฐ”๋ฅธ ์‹œ๊ฐ„๊ณผ ์˜ฌ๋ฐ”๋ฅธ ์ธ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ์˜ ๊ฐ์ฒด๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ๋ฟ์ž…๋‹ˆ๋‹ค.



๋Œ€์•ˆ:

  • ์ปจํ…Œ์ด๋„ˆ + ์„ ํƒ๊ธฐ๋ฅผ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ๋ชจ์˜ ์ €์žฅ์†Œ ์ƒํƒœ
  • ์ปจํ…Œ์ด๋„ˆ + ์ž‘์—…์„ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด ์™ธ๋ถ€ API ํ˜ธ์ถœ ๋ชจ์˜

@AnaRobynn @ucorina ์˜ ๋‘ ๋ฒˆ์งธ ์ ‘๊ทผ ๋ฐฉ์‹์ด ์˜ฌ๋ฐ”๋ฅธ ์ ‘๊ทผ ๋ฐฉ์‹์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๊ฐ€ ๋“ค์€ ๋งŽ์€ ๊ฒƒ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. mapDispatchToProps๋ฅผ ๋…ธ์ถœํ•˜๊ณ  ์ง์ ‘ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  2. connect()๊ฐ€ ์ด๋ฏธ ํ…Œ์ŠคํŠธ๋˜์—ˆ์œผ๋ฏ€๋กœ connect()์— ๋Œ€ํ•œ ํ˜ธ์ถœ์„ ํ…Œ์ŠคํŠธํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

1๊ณผ ๊ด€๋ จํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋‚ด๋ถ€๋ฅผ ๋…ธ์ถœํ•˜๋Š” ๊ฒƒ์€ ์ข‹์€ ๋ฐฉ๋ฒ•์ด ์•„๋‹ˆ๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ A: ์ด์ œ ๋‚ด๋ถ€ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ •ํ•˜๊ณ  B: ์‹ค์ œ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋ฐฉ์‹์„ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

2์— ๊ด€ํ•ด์„œ๋Š” _์ ˆ๋Œ€์ ์œผ๋กœ_ ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์„ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ๊นจ๋Š” ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์˜ ์•ฝ์  ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๋งˆ์ด๋„ˆ/ํŒจ์น˜ ๋ฒ„์ „ ๋ฒ”ํ”„์— ์ฃผ์š” ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ๋„์ž…๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ ํ…Œ์ŠคํŠธ๊ฐ€ ๋นจ๋ฆฌ ์‹คํŒจํ•˜๊ธฐ๋ฅผ _์›ํ•ฉ๋‹ˆ๋‹ค_.

'์˜ต์…˜ 2'๊ฐ€ ์•ก์…˜ ์ œ์ž‘์ž๋ฅผ ๊ฐ„์ ‘์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ๋„ ๊ดœ์ฐฎ๋‹ค๊ณ  ๋ณด์‹ญ๋‹ˆ๊นŒ? ๋˜ํ•œ ๊ฐ„์ ‘์ ์œผ๋กœ ์„ ํƒ๊ธฐ๋ฅผ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๊นŒ? ์ข‹์€ ์ผ์ด์•ผ?

์˜ˆ, ์ค‘๋ณต ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ์ข‹์€ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

@philihp @dougbacelar
๋‚˜๋Š” React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ…Œ์ŠคํŠธ์™€ ๊ด€๋ จํ•˜์—ฌ ์กฐ๊ธˆ ์„ฑ์žฅํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๋‚˜์˜ ํ˜„์žฌ ์‹ ๋…๊ณผ ์ฃผ์ œ์— ๋Œ€ํ•œ ์ƒ๊ฐ:

  1. ์—ฌ๋Ÿฌ ๊ตฌ์„ฑ ์š”์†Œ์—์„œ ์ด๋Ÿฌํ•œ ๋งคํ•‘์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ mapStateToProps ๋ฐ mapDispatchToProps ๋ฅผ ๋…ธ์ถœํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค. ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ๋งŒ ๋‚ด๋ณด๋‚ด๋Š” ๊ฒƒ์€ ๋ฐ˜ํŒจํ„ด์ž…๋‹ˆ๋‹ค.
  2. ์ €๋Š” ํ˜„์žฌ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํ˜‘๋ ฅ์ž(๋‹ค๋ฅธ ๊ธฐ๋Šฅ์„ ์œ„์ž„ํ•˜๋Š” ๊ธฐ๋Šฅ๊ณผ ์‹ค์ œ ๋…ผ๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๋‹ค๋ฅธ ๊ธฐ๋Šฅ)๋กœ ๋ด…๋‹ˆ๋‹ค.
    ๊ทธ๊ฒŒ ๋ฌด์Šจ ๋œป์ด์•ผ?
  3. ์ปจํ…Œ์ด๋„ˆ๋Š” ์–ด๋–ค ๋…ผ๋ฆฌ๋„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  4. ์ƒํƒœ๋ฅผ ์„ ํƒํ•˜๋Š” ๋…ผ๋ฆฌ๋Š” ์„ ํƒ๊ธฐ ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค(์ˆœ์ „ํžˆ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ).
  5. ์•ก์…˜์— ๋Œ€ํ•œ ๋…ผ๋ฆฌ๋Š” ์•ก์…˜ ์ƒ์„ฑ์ž์— ์ˆจ๊ฒจ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค(redux-thunk๋ฅผ ์‚ฌ์šฉํ•˜๋Š”์ง€ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๊ทธ ์ž์ฒด๋กœ ๊ณต๋™ ์ž‘์—…์ž๊ฐ€ ๋  ์ˆ˜ ์žˆ์Œ)
  6. ๋ Œ๋”๋ง๋œ ๋ทฐ๋Š” ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค(์ž ์žฌ์ ์œผ๋กœ ๋‹ค๋ฅธ ํ˜‘๋ ฅ์ž ๊ธฐ๋Šฅ์ด ์žˆ๊ฑฐ๋‚˜ ์ˆœ์ˆ˜ํ•˜๊ฒŒ ๋ฌผ๊ฑด์„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

=> ์ ์ ˆํ•œ ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ํ…Œ์ŠคํŠธ ์„ค์ •์€ ์‹ค์ œ๋กœ ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค(testdouble.js ๊ถŒ์žฅ).
=> testdouble.js๋ฅผ ํ†ตํ•ด ์„ ํƒ๊ธฐ์™€ ์ž‘์—…์„ ์กฐ๋กฑํ•˜์‹ญ์‹œ์˜ค( td.when() ์‚ฌ์šฉ).
=> ์ปจํ…Œ์ด๋„ˆ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์–•๊ฒŒ ๋ Œ๋”๋งํ•˜๊ณ  ๋ทฐ ๊ตฌ์„ฑ ์š”์†Œ์˜ ๋ฉ”์„œ๋“œ์— ์•ก์„ธ์Šคํ•˜๊ธฐ ์œ„ํ•ด dive() ํ•œ ๋ฒˆ
=> td.when() ์˜ ๊ทœ์น™ ์„ธํŠธ๊ฐ€ ์ฃผ์–ด์ง€๋ฉด ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธ

์•ฝ๊ฐ„์˜ fakeStore ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ฃผ์ž…ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ €์žฅ์†Œ๋ฅผ ์Šคํ… ์•„์›ƒํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์˜ˆ์‹œ:

/** <strong i="27">@format</strong> */

import React from 'react';
import { shallow } from 'enzyme';

function setup(props) {
  const HasOrganization = require('./HasOrganization').default;
  const defaultProps = {
    store: {
      subscribe: Function.prototype,
      getState: Function.prototype,
      dispatch: Function.prototype,
    },
  };
  const container = shallow(<HasOrganization {...defaultProps} {...props} />);
  return {
    container,
    wrapper: container.dive(),
  };
}

describe('<HasOrganization /> collaborations', () => {
  let getCurrentOrganizationId;
  beforeEach(() => {
    getCurrentOrganizationId = td.replace('../containers/App/userSelectors')
      .selectCurrentOrganizationId;
  });

  test('not render anything when the id is not valid', () => {
    const Dummy = <div />;
    td.when(getCurrentOrganizationId()).thenReturn(null);
    const { wrapper } = setup({ children: Dummy });

    expect(wrapper.find('div').length).toBe(0);
  });

  test('render something when the id is valid', () => {
    const Dummy = <div />;
    td.when(getCurrentOrganizationId()).thenReturn(1);
    const { wrapper } = setup({ children: Dummy });

    expect(wrapper.find('div').length).toBe(1);
  });
});

์ƒ๊ฐ?

@AnaRobynn 1๋ฒˆ๊ณผ 2๋ฒˆ ํ•ญ๋ชฉ์— ์ „์ ์œผ๋กœ ๋™์˜ํ•ฉ๋‹ˆ๋‹ค!

๋‚ด ์ปจํ…Œ์ด๋„ˆ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.

// imports hidden

const mapStateToProps = (state, { userId }) => ({
  isLoading: isLoading(state),
  hasError: hasError(state),
  placeholders: getPlaceholders(userId)(state), // a list of some sort
  shouldDoStuff: shouldDoStuff(state),
});

const mapDispatchToProps = {
  asyncActionPlaceholder,
};

export const mergeProps = (stateProps, dispatchProps, ownProps) => {
  return {
    ...stateProps,
    ...dispatchProps,
    ...ownProps,
    onMount: () => {
      if (stateProps.shouldDoStuff) {
        dispatchProps.asyncActionPlaceholder(ownProps.userId);
      }
    },
  };
};

export default compose(
  connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps
  ),
  callOnMount(props => props.onMount())
)(SampleComponent);

๊ทธ๋ž˜์„œ ๋‚˜๋Š”

  1. ๋ชจ๋“  ์„ ํƒ์ž์™€ ์ž‘์—… ์ƒ์„ฑ์ž๋ฅผ ๊ฐ์‹œํ•˜์‹ญ์‹œ์˜ค.
  2. ํ…Œ์ŠคํŠธ getPlaceholders ์ŠคํŒŒ์ด๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ userId๋กœ ํ˜ธ์ถœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  3. SampleComponent ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ์†Œํ’ˆ์œผ๋กœ ๋ Œ๋”๋ง๋˜์—ˆ๋‹ค๊ณ  ์ฃผ์žฅํ•ฉ๋‹ˆ๋‹ค.
  4. SampleComponent onMount shouldDoStuff===true ๋•Œ ๋ชจ์˜ ์ž‘์—…์„ ์ „๋‹ฌํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • SampleComponent ์— ๋Œ€ํ•œ ๋ณ„๋„์˜ ํ…Œ์ŠคํŠธ์—์„œ ํ…Œ์ŠคํŠธํ•  props์— ๋”ฐ๋ผ ํ•ญ๋ชฉ์˜ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง๊ณผ ๊ฐ™์€ ๊ฒƒ
  • ๋‚˜๋Š” ๋˜ํ•œ configureMockStore([thunk])() ์™€ ๊ฐ™์€ ์ƒ์ ์„ ์กฐ๋กฑํ•˜๊ธฐ ์œ„ํ•ด redux-mock-store ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ์„ ํ˜ธํ•˜์ง€๋งŒ ๊ทธ๋ ‡๊ฒŒ ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค...

TL, DR ๊ธฐ๋ณธ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ์„ ํƒ๊ธฐ๋งŒ ์˜ฌ๋ฐ”๋ฅธ ์ธ์ˆ˜๋กœ ํ˜ธ์ถœ๋˜์—ˆ๊ณ , ๋ชจ์˜ ์ž‘์—…์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ „๋‹ฌ๋˜์—ˆ์œผ๋ฉฐ(์–ด๋–ค ๋…ผ๋ฆฌ์— ์˜์กดํ•˜๋Š” ๊ฒฝ์šฐ) ํ•˜์œ„ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ props๋กœ ๋ Œ๋”๋ง๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

@philihp

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

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

@dougbacelar

๋‚˜๋Š” ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์„ ๋งค์šฐ ๋ฐ˜๋Œ€ํ•˜๋ฉฐ ๋ชจ๋“  ๊ฒƒ์„ ์กฐ๋กฑํ•˜๊ณ  ๊ฐœ๋ณ„์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์„ ์„ ํ˜ธํ•ฉ๋‹ˆ๋‹ค.

ํ•„์š”ํ•  ๋•Œ ์กฐ๋กฑํ•˜์ง€๋งŒ ์‹ค์ œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ๊ทธ ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? ์กฐ๋กฑ์€ ํŠน์ • ํ˜ธ์ถœ์ด ๋Š๋ฆฌ๊ฑฐ๋‚˜ ๋„คํŠธ์›Œํฌ ์š”์ฒญ๊ณผ ๊ฐ™์€ ๋ถ€์ž‘์šฉ์ด ์žˆ์„ ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๊ฐ€ ์ปจํ…Œ์ด๋„ˆ+์„ ํƒ๊ธฐ+์•ก์…˜ ์ƒ์„ฑ๊ธฐ๋ฅผ ๋ชจ๋‘ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์„ ์ข‹์•„ํ•˜์ง€ ์•Š๋Š” ์ด์œ ๋Š” ์žฌ์‚ฌ์šฉํ•˜๋Š” ๋‹ค๋ฅธ ๋ชจ๋“  ์ปจํ…Œ์ด๋„ˆ์— ๋Œ€ํ•ด ๋™์ผํ•œ ์„ ํƒ๊ธฐ์™€ ์•ก์…˜ ์ƒ์„ฑ๊ธฐ๋ฅผ ๋‹ค์‹œ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทธ ์‹œ์ ์—์„œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž์ฒด์— ๋” ๋งŽ์€ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์˜ˆ , ์„ ํƒ์ž, ์ž‘์—… ์ž‘์„ฑ์ž๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค. ๋งŽ์€ ์˜ˆ์ƒ ์ž…๋ ฅ์œผ๋กœ ์ฒ ์ €ํžˆ ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค.

์˜ˆ , ์ปจํ…Œ์ด๋„ˆ๋„ ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค. ๊ทธ๊ฒƒ์ด ์„ ํƒ์ž์™€ ์ž‘์—… ์ƒ์„ฑ์ž์— ๋Œ€ํ•ด ์ค‘๋ณต๋˜๋Š” ์ ์šฉ ๋ฒ”์œ„๋ฅผ ์ œ๊ณตํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค๋ฉด ๊ทธ๊ฒƒ์€ ๋‚˜์œ ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค.

์—ฌ๊ธฐ ๋‚ด ๋ง์€ ...

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { saveColor } from '../actions/save';
import { selectColors } from '../reducers/colors.js';
import ColorButtons from '../components/ColorButtons';
const ColorButtons = ({ colors, onClick }) => (
  <div>
    {colors.map(color => {
      <button type="button" key={color} onClick={onClick(color)}>{color}</button>
    })}
  </div>
);
ColorButtons.propTypes = {
  colors: PropTypes.arrayOf(PropTypes.string),
  onClick: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
  colors: selectColors(state.colors),
});
const mapDispatchToProps = dispatch => ({
  onClickColor: color => () => {
    dispatch(saveColor({ color }));
  },
});
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ColorButtons);
import React from 'react';
import { shallow } from 'enzyme';
import configureStore from 'redux-mock-store';
import ColorButtons from '../ColorButtons';
import { saveColor } from '../../actions/save';
import { selectColors } from '../../reducers/colors.js';
const buildStore = configureStore();
describe('<ColorButtons />', () => {
  let store;
  let wrapper;
  const initialState = { colors: ['red', 'blue'] };
  beforeEach(() => {
    store = buildStore(initialState);
    wrapper = shallow();
  });
  it('passes colors from state', () => {
    expect(wrapper.props().colors).toEqual(selectColors(initialState.colors));
  });
  it('can click yellow', () => {
    const color = 'yellow';
    wrapper.props().onClick(color)();
    expect(store.getActions()).toContainEqual(saveColor({ color }));
  });
});

์—ฌ๊ธฐ์„œ ์ด๊ฒƒ์€ selectColors ๋ฐ saveColor ๊ฐ€ ์–ป์„ ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ๋‹ค๋ฅธ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. <ColorButtons /> ๊ฐ€ ์ƒ์„ฑ๋  ๋•Œ ์–ป์„ ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€ํ•˜๋Š” ์†์„ฑ์„ ๊ฐ€์ ธ์˜ค๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์—์„œ ์ ˆ๋Œ€์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค.

import { selectColors } from '../colors.js';
describe('selectColors', () => {
  it('returns the colors', state => {
    expect(selectColors(['red'])).toEqual(['red'])
  })
  it('returns an empty array if null', state => {
    expect(selectColors(null)).toEqual([])
  })
  it('returns a flattened array', state => {
    expect(selectColors(['red', ['blue', 'cyan']])).toEqual(['red','blue','cyan'])
  })
})

๋‚˜๋Š” ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์„ ๋งค์šฐ ๋ฐ˜๋Œ€ํ•˜๋ฉฐ ๋ชจ๋“  ๊ฒƒ์„ ์กฐ๋กฑํ•˜๊ณ  ๊ฐœ๋ณ„์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์„ ์„ ํ˜ธํ•ฉ๋‹ˆ๋‹ค.

ํ•„์š”ํ•  ๋•Œ ์กฐ๋กฑํ•˜์ง€๋งŒ ์‹ค์ œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ๊ทธ ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? ์กฐ๋กฑ์€ ํŠน์ • ํ˜ธ์ถœ์ด ๋Š๋ฆฌ๊ฑฐ๋‚˜ ๋„คํŠธ์›Œํฌ ์š”์ฒญ๊ณผ ๊ฐ™์€ ๋ถ€์ž‘์šฉ์ด ์žˆ์„ ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋ถ€๋ถ„์ ์œผ๋กœ๋Š” ๋™์˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
๋‚˜๋Š” ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์กฐ๋กฑํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค(์ž์‹ ์ด ์†Œ์œ ํ•˜์ง€ ์•Š์€ ๊ฒƒ์„ ์กฐ๋กฑํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค). ํ•˜์ง€๋งŒ axios์™€ ๊ฐ™์€ ๋ž˜ํผ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ํ˜‘์—… ํ…Œ์ŠคํŠธ์—์„œ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์กฐ๋กฑํ•˜๊ณ  ๋ชจ๋“  ๊ฒƒ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์—ฐ๊ฒฐ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

์—ฌ๊ธฐ ๋‚ด ๋ง์€ ...

๋˜ํ•œ ๋™์˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
์—ฌ๊ธฐ์—์„œ ๊ฐ์†๊ธฐ์™€ ์„ ํƒ๊ธฐ๋ฅผ ์•”์‹œ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์ด ๋‹น์‹ ์ด ์ž˜ํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ์ด์ง€๋งŒ ๋‚˜๋Š” ์ด๋Ÿฌํ•œ ํ•จ์ˆ˜๋ฅผ ์กฐ๋กฑํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ˜ธ์ถœ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ์„ ์„ ํ˜ธํ•ฉ๋‹ˆ๋‹ค. ๋‚˜๋จธ์ง€๋Š” ๊ฐ์†๊ธฐ/์„ ํƒ๊ธฐ์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

์ด ์Šค๋ ˆ๋“œ๋Š” https://martinfowler.com/articles/mocksArentStubs.html , Classical and Mockist Testing ์—์„œ ๋งŒ๋“  ํฌ์ธํŠธ๋ฅผ ๋‹ค์‹œ ํ•ด์‹ฑํ•ฉ๋‹ˆ๋‹ค.

๋™์˜. ์ž ๊ธˆ.

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰