728x90

JavaScript와 HTML,CSS등에 대해서는 일체 다루지 않는다.

기초 지식은 다른 강의를 참조하도록 하라.

이 강의는 React의 사용법 위주로 작성되어 있다.

왜 React여야는지, React를 써야하는지에 대하지는 논하지 않으므로 그런 자료는 다른 데이터를 참조하라.


참조:

react docs 


React10강 포스팅에서 양방향 바인딩에 대해서 알아보았다.

그러나 이 방식에 치명적인 문제점이 있다고 하였다.

이를 알아보도록 하자.



부모는 자식에게 props로, 자식은 부모에게 부모가 하사해준 함수로 서로 영향을 미칠 수 있다.

하지만 위를 예로 들어보자. A가 바뀐걸 E에 반영한다고 가정해보자.

그렇다면 우리는 A의 변화를 B에 전달, B에서 C로 전달, C에서 D로 전달, D에서 E로 전달해야한다.


이는 장단점이 있다.


장점

1.모듈화가 가능하다. 통째로 덜어내서 붙혀넣기를 해도 잘 작동한다.

2.책임소지가 명확해서 디버깅이 쉽다.


단점

1.이벤트로 인한 함수가 너무 많이 호출된다.

2.깊이 깊어지면 디버깅이 어렵진 않은데 귀찮아진다.

3.필요없는 값이 복제가 일어난다.

4.코딩하기 빡세다.


사실 4번이 제일 중요한건 아니지만 여러분이 가장 잘 와닿는 부분일 거라서 bold+italic처리를 했다.


그리고 여러분이 코딩을 해보면 알겠지만 부모가 자식에게 props로 변화를 시키는건 비교적 간단한 작업이지만

자식이 부모에게 함수로 변화를 주는건 코딩이 귀찮다. 그래서 필연적으로 최상위 노드를 정해서 최대한 자식->부모 형태의 값전달을 없애려고 할 것이다.

하지만 이게 생각처럼 쉽지는 않다. 가령 위의 그림에서는 C를 최상단으로 하는게 유리할 것이다.

그러면 A->B->C는 lift up으로 값을 전달하고 C->D->E는 props로 전달하는게 좋다.



하지만 이렇게 추가적인 컴포넌트가 끼어들면 루트가 뭔지 애매해진다.

이 구조의 경우 F가 최상단인게 유리하다. 결국 논리적인 구조가 깨지지만 코드가 길다면 수정하기는 애매해진다.

이런일이 반복되다 보면 결국 데이터 흐름 구조가 스파게티 같이 바뀌게 된다.


초기에 많은 개발자들이 여기에 대해서 엄청난 고민을 한것 같다.

왜냐하면 책임소재를 명확히 하기위해서 단방향 바인딩을 고수하고 인접한 값들끼리 바꾸기로 했다.

그렇지만 이 구조가 깔끔하지 않고 문제를 일으키기 딱 좋은 구조이기 때문이다.


그래서 등장한게 flux디자인이고 이를 참조해서 만든게 redux이다.

여기서 flux가 뭔지에 대해서는 짧막하게 보자.


자 짧막하게 봤으니까 됬다.

농담이고 밑에서 자세히 설명하겠다.


redux를 사용하기 전에 예제를 보도록 하자.



아주, 정말 아주 간단한 예제다.

+버튼을 누르면 숫자가 증가하고 -버튼을 누르면 숫자가 감소한다.


Lifting state up으로 구현


// App.js
import React, {Component} from 'react'
import AddButton from './AddButton'
import SubButton from './SubButton'

class App extends Component {
state = {
num: 0
};

constructor(props) {
super(props);
}

render() {
return (
<div className="App">
<span>{this.state.num}</span><br/>
<AddButton addNumber={this.addNumber}/><SubButton subNumber={this.subNumber}/>
</div>
);
}

addNumber = () => {
this.setState({
num: this.state.num + 1
})
};

subNumber = () => {
this.setState({
num: this.state.num - 1
})
};
}

export default App;

일단 최상위인 App의 구조이다.

특이 할건 없고 AddButton과 SubButton에 각각 lift up 함수를 넘겨준다.


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

class AddButton extends Component{
constructor(props){
super(props);
}

render(){
return(
<input value={'+'} type="button" onClick={this.props.addNumber}/>
)
}
}

export default AddButton;

그리고 +버튼을 누르는 이벤트가 발생하면 위로 데이터를 넘긴다.


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

class SubButton extends Component{
constructor(props){
super(props);
}

render(){
return(
<input value={'-'} type="button" onClick={this.props.subNumber}/>
)
}
}

export default SubButton;

-버튼도 마찬가지다.

보면 아주아주 쉬운 예제다.


이제 redux를 사용한 예제로 바꿔보자.


redux를 사용해서 구현



npm install --save redux


당연하지만 먼저 redux를 깔아준다.


redux를 사용할 때는 먼저 행위라는게 존재해야한다.

이를 우리는 action이라고 하자.



우리는 actions 라는 파일, 혹은 actions 밑에 index로 만들어준다.

둘중 뭐로 만들어도 상관은 없으나 아마 대부분 프로젝트에서는 위처럼 만들어주는게 좋다.

왜 그런지는 알거라 믿는다.(그냥 규모가 커지면 한 파일에 담을 수는 없으니까)


// actions/index.js
export const ADD = 'ADD';
export const SUB = 'SUB';
export const add = () => {
return {
type: ADD
}
};

export const sub = () => {
return {
type: SUB
}
};

actions에서는 각각의 행위에 대한 정의를 해주면된다.

뭐 쉽게 이야기하면 커스텀 이벤트를 만드는 과정이라고 해도 무방하다.

우리는 두개의 action(event라 생각해도 됨)을 만들건데 그게 add와 sub다.

둘은 함수이므로 파라메터를 받을 수도 있다.

위 예제에서는 생략되있으나 당연히 파라메터가 값에 영향을 미칠 수 있다.



reducers 역시 만들고 index.js를 만들어준다.


// reducers/index.js
import {ADD, SUB} from '../actions'
import {combineReducers} from 'redux'

const initState = {
number: 0,
};

const data = (state = initState, action) => {
switch (action.type) {
case ADD:
return Object.assign({}, state, {
number: state.number + 1
});
case SUB:
return Object.assign({}, state, {
number: state.number - 1
});
default:
return state;
}
};

const App = combineReducers({
data
});

export default App;

reducers는 flux디자인에서 dispatcher역활을 한다.

하나하나 보도록 하자.


const initState = {
number: 0,
};

일단 reducers는 하나의 또다른 컴포넌트라고 생각하면 편하다.

이 녀석도 state를 가진다.

그리고 우리는 state를 초기화 해줄 것이다.


const data = (state = initState, action) => {
switch (action.type) {
case ADD:
return Object.assign({}, state, {
number: state.number + 1
});
case SUB:
return Object.assign({}, state, {
number: state.number - 1
});
default:
return state;
}
};

그 다음 우리는 일종의 dispatcher를 만들것이다.

보통 위같은 형태로 만든다.

우리는 특정 액션이 여기를 거쳐서 넘어와서 해당 타입이 맞을경우(즉 해당 액션이 맞을 경우)각각 캐이스에 맞는 작업을 수행한다.


const App = combineReducers({
data
});

그 다음 위처럼 리듀서로 등록해주면 준비는 끝... 나면 좋으려면 아직 남았다.


// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import {createStore} from 'redux';
import reducers from './reducers';

const store = createStore(reducers);

const render = ()=> {
ReactDOM.render(
<App store={store}/>,
document.getElementById('root')
);
};
store.subscribe(render);
render();

그리고 src/index.js에서 아래처럼 코드를 바꿔준다.

마지막에 subscribe구문이 새로 추가됬는데 왜 그렇냐면

저 구문없이 동작하면 화면이 재렌더링이 되지않고 값만 바뀐다.


store.subscribe(render);

그래서 이 선언은 우리는 앞으로 redux의 선언으로 값만 바뀌는게 아니라 반응해서 렌더링 까지 새로할거라는 의미이다.


const render = ()=> {
ReactDOM.render(
<App store={store}/>,
document.getElementById('root')
);
};

그리고 이제부터 중요해진게 redux를 사용할 모든 녀석에게는 반드시 store를 연결해준다.

사실... 그냥 redux쓸꺼면 모든 녀석에게 연결해주는게 마음편하다.


// App.js
import React, {Component} from 'react'
import AddButton from './AddButton'
import SubButton from './SubButton'

class App extends Component {

constructor(props) {
super(props);
}

render() {
return (
<div className="App">
<span>{this.props.store.getState().data.number}</span><br/>
<AddButton store={this.props.store}/><SubButton store={this.props.store}/>
</div>
);
}
}

export default App;

App.js를 보면 코드가 좀 수정됬다. 일단 store를 사용하는 모든 녀석에게 store를 붙혀줘야한다고 하였다.


<span>{this.props.store.getState().data.number}</span><br/>

이 코드는 위의 reducers를 보면 기억하겠지만 redcuers가 가지고 있는 state값을 호출하는것이다.

놀랍게도 우리는 이 녀석이 바뀌면 모두 재렌더링되게 된다.


// AddButton.js
import React, {Component} from 'react';
import {add} from './actions'

class AddButton extends Component {
constructor(props) {
super(props);
}

render() {
return (
<input value={'+'} type="button" onClick={this.addNumber}/>
)
}

addNumber = () => {
this.props.store.dispatch(add());
}
}

export default AddButton;

그리고 AddButton도 바뀌었다.


addNumber = () => {
this.props.store.dispatch(add());
}

버튼을 클릭했을때 store에 디스패치하게 바뀌었다.

그냥 쉽게 말해서 actions에서 선언한 add함수를 호출하면 이녀석이 reducers로 넘어가서 타입에 맞게 알아서 함수가 실행된다.


// SubButton.js
import React, {Component} from 'react';
import {sub} from './actions'

class SubButton extends Component {
constructor(props) {
super(props);
}

render() {
return (
<input value={'-'} type="button" onClick={this.subNumber}/>
)
}

subNumber = () => {
this.props.store.dispatch(sub());
}
}

export default SubButton;

SubButton역시 마찬가지이다.


이제 실행하면 저번과 같이 똑같이 실행된다.


redux동작원리



redux에서 보면 위와 같은 구조라고 하였는데 예제와 함께 생각을 해보자.


addNumber = () => {
this.props.store.dispatch(add());
}

우리가 AddButton을 누르면action(이벤트라 생각해도 됨)을 dispatch를 한다.

redux에서는 dispatcher와 reducer는 같은 개념이며 dispatch한다는 말은 reducer로 보낸다는 의미와 동일하다.


const data = (state = initState, action) => {
switch (action.type) {
case ADD:
return Object.assign({}, state, {
number: state.number + 1
});
case SUB:
return Object.assign({}, state, {
number: state.number - 1
});
default:
return state;
}
};

그러면 reducer에서는 해당 액션을 찾아서 state를 변화시킨다.

이 state는 전역이라고 봐도 무방하다. 사실상 redux를 쓰는 이유!


store.subscribe(render);

우리가 해놓은 설정으로 인해서 값이 바뀌면 렌더링이 다시 일어난다.


<div className="App">

<span>{this.props.store.getState().data.number}</span><br/>
<AddButton store={this.props.store}/><SubButton store={this.props.store}/>
</div>

그러면 값이 바뀌면서 반영이 된다.


redux는 생각보다 직관적이지는 못한편이다.

왜 이렇게 만들었는지는 observer pattern을 이해해야하나 그것까지 설명하면 길어지므로 설명을 여기서 마치겠다.

+ Recent posts