<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 ์ด๋ค ์์ด๋์ด๊ฐ ์์ต๋๊น?
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
์ธ๋ถ์ ์์ฒด ํจ์๋ก ์ ์ํ๊ธฐ๋ฅผ ์ํ ๊ฒ์
๋๋ค.dispatch
์ ์ก์ธ์คํ ์ ์์ต๋๋ค.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
๊ทธ๋ฆฌ๊ณ ์์์ ๋ค๋ฅธ ์ฌ๋๋ค์ด ์ง์ ํ๋ฏ์ด ์ฐ๊ฒฐ์ ์ ๋ฌํ๋ ๊ธฐ๋ฅ์ ๋ํ ์ ์ฉ ๋ฒ์๊ฐ ์๋ค๋ฉด ๊ตฌ์ฑ ์์ ์ ์ฒด์ ๋ํ ์ ์ฒด ์ ์ฉ ๋ฒ์๊ฐ ์์ด์ผ ํฉ๋๋ค.
์, ์ฌ๊ธฐ์ ๊ฐ์ฅ ๋จผ์ ์ฃผ๋ชฉํด์ผ ํ ๊ฒ์
๋ด๊ฐ ๊ด์ฐฎ๋ค๋ฉด ๋ค์ ๋ ๊ฐ์ง๋ง ํ ์คํธํ๋ฉด ๋ฉ๋๋ค.
์ด๊ฒ์ด ๋ด ์ ๊ทผ ๋ฐฉ์์ ๋๋ค.
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
๋ฅผ ์ง์ ํ
์คํธํด์๋ ์ ๋๋ค๋ ๋ฐ ๋์ํฉ๋๋ค. ๋จ์ํ ํ
์คํธ ๋ชฉ์ ์ผ๋ก ์ด๋ฌํ ๊ฐ์ธ ๋ฉ์๋๋ฅผ ๋
ธ์ถํ๋ ๊ฒ์ ๋์๊ฒ ํญ์ ์ฝ๋ ๋์์ฒ๋ผ ๋๊ปด์ก์ต๋๋ค. ์ฌ์ค, ์ฐ๊ฒฐ๋ ๊ตฌ์ฑ ์์๋ฅผ ํ
์คํธํด์ผ ํ๋ ์ ์ผํ ์๊ฐ์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค. ์์ฝํ์๋ฉด:
mapStateToProps
๋๋ mapDispatchToProps
์์ถ ๊ธ์ง4๋ฒ์ ์ํํ ๋์ ์ ์ผํ ์ถ๊ฐ ์ค๋ฒํค๋๋ ์ฐ๊ฒฐ๋ ๊ตฌ์ฑ ์์์ ์ ๋ฌํ๊ธฐ ์ํด redux ์ ์ฅ์๋ฅผ ์ ๊ฑฐํด์ผ ํ๋ค๋ ๊ฒ์ ๋๋ค. ํ์ง๋ง API๊ฐ ์ธ ๊ฐ์ง ๋ฐฉ๋ฒ์ ๋ถ๊ณผํ๊ธฐ ๋๋ฌธ์ ์ด๊ฒ์ ๋งค์ฐ ์ฝ์ต๋๋ค.
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๊ฐ์ง ์ ๊ทผ ๋ฐฉ์์ด ์๋ค๋ ๊ฒฐ๋ก ์ ๋๋ฌํ์ต๋๋ค.
mapDispatchToProps
๋ฐ mapStateToProps
๋ด๋ณด๋ด๊ธฐ ๋ฐ ๋ณ๋๋ก ํ
์คํธ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% ํ์ ํ ์๋ ์์ง๋ง ๊ฑฐ๊ธฐ์ ์๋ ๋ชจ๋ (์๋ชป๋) ์ ๋ณด์ ๋ํด์๋ ํผ๋์ค๋ฝ์ต๋๋ค. ํ
์คํธ์ ๋ํ ๋ค์ํ ์์ด๋์ด๊ฐ ์์ต๋๋ค.
mapStateToProps
๋ด๋ถ์ ์ ํ๊ธฐ๋ง ์ฌ์ฉmapDispatchToProps
๋ด์์ ์ก์
์์ฑ์๋ง ์ฌ์ฉmapStateToProps
, mapDispatchToProps
๋๋ mergeProps
์ ์ถ๊ฐํด์ผ ํ๊ณ ๋ค๋ฅธ ๊ณณ์์๋ ์์ง ํ
์คํธ๋์ง ์์ ์ถ๊ฐ ๋
ผ๋ฆฌ๋ฅผ ํ
์คํธํฉ๋๋ค. (์กฐ๊ฑด๋ถ ๋์คํจ์น, ownProps
์์ ์ธ์๋ฅผ ๋ฐ๋ ์ ํ๊ธฐ ๋ฑ)๋์:
@AnaRobynn @ucorina ์ ๋ ๋ฒ์งธ ์ ๊ทผ ๋ฐฉ์์ด ์ฌ๋ฐ๋ฅธ ์ ๊ทผ ๋ฐฉ์์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค.
๋ด๊ฐ ๋ค์ ๋ง์ ๊ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
1๊ณผ ๊ด๋ จํ์ฌ ํ ์คํธ๋ฅผ ์ํด ๋ด๋ถ๋ฅผ ๋ ธ์ถํ๋ ๊ฒ์ ์ข์ ๋ฐฉ๋ฒ์ด ์๋๋ผ๊ณ ์๊ฐํฉ๋๋ค. ํ ์คํธ A: ์ด์ ๋ด๋ถ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ ํ๊ณ B: ์ค์ ๋ก ์ฌ์ฉ๋๋ ๋ฐฉ์์ ํ ์คํธํด์ผ ํฉ๋๋ค.
2์ ๊ดํด์๋ _์ ๋์ ์ผ๋ก_ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํธ์ถํ๋ ๊ฒ์ ํ ์คํธํด์ผ ํฉ๋๋ค. ์ด๊ฒ์ ๊นจ๋ ์์ฉ ํ๋ก๊ทธ๋จ์ ์ฝ์ ์ค ํ๋์ ๋๋ค. ๋ชจ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ๋ง์ด๋/ํจ์น ๋ฒ์ ๋ฒํ์ ์ฃผ์ ๋ณ๊ฒฝ ์ฌํญ์ด ๋์ ๋ ์ ์์ผ๋ฉฐ ํ ์คํธ๊ฐ ๋นจ๋ฆฌ ์คํจํ๊ธฐ๋ฅผ _์ํฉ๋๋ค_.
'์ต์ 2'๊ฐ ์ก์ ์ ์์๋ฅผ ๊ฐ์ ์ ์ผ๋ก ํ ์คํธํ๋ ๊ฒ๋ ๊ด์ฐฎ๋ค๊ณ ๋ณด์ญ๋๊น? ๋ํ ๊ฐ์ ์ ์ผ๋ก ์ ํ๊ธฐ๋ฅผ ํ ์คํธํฉ๋๊น? ์ข์ ์ผ์ด์ผ?
์, ์ค๋ณต ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ ์ข์ ๊ฒ์ ๋๋ค.
@philihp @dougbacelar
๋๋ React ์ ํ๋ฆฌ์ผ์ด์
ํ
์คํธ์ ๊ด๋ จํ์ฌ ์กฐ๊ธ ์ฑ์ฅํ๋ค๊ณ ์๊ฐํฉ๋๋ค. ๋์ ํ์ฌ ์ ๋
๊ณผ ์ฃผ์ ์ ๋ํ ์๊ฐ:
mapStateToProps
๋ฐ mapDispatchToProps
๋ฅผ ๋
ธ์ถํ์ง ๋ง์ญ์์ค. ํ
์คํธ์ฉ์ผ๋ก๋ง ๋ด๋ณด๋ด๋ ๊ฒ์ ๋ฐํจํด์
๋๋ค.=> ์ ์ ํ ํ
์คํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๋ ํ
์คํธ ์ค์ ์ ์ค์ ๋ก ๋งค์ฐ ๊ฐ๋จํฉ๋๋ค(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);
๊ทธ๋์ ๋๋
getPlaceholders
์คํ์ด๊ฐ ์ฌ๋ฐ๋ฅธ userId๋ก ํธ์ถ๋์์ต๋๋ค.SampleComponent
๊ฐ ์ฌ๋ฐ๋ฅธ ์ํ์ผ๋ก ๋ ๋๋ง๋์๋ค๊ณ ์ฃผ์ฅํฉ๋๋ค.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 ์์ ๋ง๋ ํฌ์ธํธ๋ฅผ ๋ค์ ํด์ฑํฉ๋๋ค.
๋์. ์ ๊ธ.
๊ฐ์ฅ ์ ์ฉํ ๋๊ธ
Redux์ ์ง์ ์ ์ธ ๊ด๋ จ์ ์์ง๋ง. ์ปจํ ์ด๋์์
shallow()
๋ฅผ ๋ค์ ํธ์ถํ์ฌ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ต๋๋ค. ์คํ ์ด ์ํ๊ฐ ์ ๋ฌ๋ ๋ด๋ถ ๊ตฌ์ฑ ์์๋ฅผ ๋ ๋๋งํฉ๋๋ค.์์:
๋์์ด ๋์๊ธฐ๋ฅผ ๋ฐ๋๋๋ค