Redux를 이용한 React앱 개발
1. 정의
- redux 를 이용하여 영화 리스트를 출력하는 앱을 개발한다.
2. 프로젝트 설정
1) package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | {
"name" : "redux-netfix" ,
"version" : "0.0.1" ,
"description" : "A sample project in React and Redux that copies Netflix's features and workflow" ,
"main" : "./build/index.js" ,
"scripts" : {
"test" : "echo \"Error: no test specified\" && exit 1" ,
"start" : "concurrently \"webpack --watch --config webpack.config.js\" \"webpack-dev-server\""
},
"repository" : {
"type" : "git" ,
},
"license" : "MIT" ,
"bugs" : {
},
"devDependencies" : {
"babel-core" : "6.11.4" ,
"babel-eslint" : "6.1.2" ,
"babel-loader" : "6.2.4" ,
"babel-polyfill" : "6.9.1" ,
"babel-preset-es2015" : "6.9.0" ,
"babel-preset-react" : "6.11.1" ,
"babel-preset-stage-0" : "6.5.0" ,
"concurrently" : "2.2.0" ,
"css-loader" : "0.23.1" ,
"eslint" : "3.1.1" ,
"eslint-plugin-babel" : "3.3.0" ,
"eslint-plugin-react" : "5.2.2" ,
"extract-text-webpack-plugin" : "1.0.1" ,
"json-loader" : "0.5.4" ,
"style-loader" : "0.13.1" ,
"webpack" : "1.13.1" ,
"webpack-dev-server" : "1.14.1" ,
"react" : "15.2.1" ,
"react-dom" : "15.2.1" ,
"react-redux" : "4.4.5" ,
"react-router" : "2.6.0" ,
"redux" : "3.5.2" ,
"redux-actions" : "0.10.1"
}
}
|
- concurrently : npm 스크립트를 더욱 빠르게 실행할 수 있다.
- extract-text-webpack-plugin : 인라인 스타일을 하나의 css파일로 만든다
- react-redux : 데이터를 다룬다.
- react-actions : Redux 리듀서를 정리한다.
2) webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | const path = require( 'path' )
const ExtractTextPlugin = require( 'extract-text-webpack-plugin' )
module.exports = {
entry: {
index: [
'babel-polyfill' ,
'./src/index.js'
]
},
output: {
path: path.join(__dirname, 'build' ),
filename: '[name].js'
},
target: 'web' ,
module: {
loaders: [{
loader: 'babel-loader' ,
include: [path.resolve(__dirname, 'src' )],
exclude: /node_modules/,
test: /\.js$/,
query: {
presets: [ 'react' , 'es2015' , 'stage-0' ]
}
}, {
loader: 'json-loader' ,
test: /\.json$/
}, {
loader: ExtractTextPlugin.extract( 'style' , 'css?modules&localIdentName=[local]__[hash:base64:5]' ),
test: /\.css$/,
exclude: /node_modules/
}]
},
resolve: {
modulesDirectories: [
'./node_modules' ,
'./src'
]
},
plugins: [
new ExtractTextPlugin( 'styles.css' )
]
}
|
3) src/index.js - Redux 사용
1 2 3 4 5 6 7 8 9 10 11 12 | const React = require( 'react' )
const {render} = require( 'react-dom' )
const {Provider} = require( 'react-redux' )
const {createStore} = require( 'react-redux' )
const reducers= require( './modules' )
const routes = require( './routes' )
module.exports = render((
<Provider store={createStore(reducers)}>
{routes}
</Provider>
), document.getElementById( 'app' ))
|
- react 애플리케이션에 redux 를 사용하려면 컴포넌트 계층의 최상위에 Provider 컴포넌트를 추가해야한다.
- Provider 컴포넌트 : react-redux 패키지의 일부로 스토어의 데이터를 컴포넌트로 주입한다. Provider 컴포넌트로 인하여 모든 자식 컴포넌트가 스토어에 접근 할 수 있다.
- store 속성 : Provider 컴포넌트를 사용하기 위해 store 속성으로 스토어를 전달한다. 스토어는 애플리케이션 상태를 표현하는 객체다.
- createStore() : modules/index.js 의 리듀서를 전달받아 스토어 객체를 반환한다.
- 자식 컴포넌트에서도 redux 를 사용하려면 connect() 라는 함수를 구현해야한다.
4) src/routes.js - 라우팅
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const React = require( 'react' )
const {
Router,
Route,
IndexRouter,
browserHistory
} = require( 'react-router' )
const App = require( 'components/app/app.js' )
const Movies = require( 'components/movies/movies.js' )
const Movie = require( 'components/movie/movie.js' )
moudle.exports = (
<Router history={browserHistory}>
<Route path= "/" component={App}>
<IndexRouter component={Movies}/>
<Route path= "movies" component={Movies}>
<Route path= ":id" component={Movie}/>
</Route>
</Route>
</Router>
)
|
3. 리듀서 사용
1) src/modules/index.js - 리듀서를 결합
1 2 3 4 5 6 7 8 9 | const {combineReducers} = require( 'redux' )
const {
reducer : movies
} = require( './moives' )
module.exports = combineReducers({
movies
})
|
- 각 리듀서는 스토어의 데이터를 변경할 수 있다. 그러나 각각의 리듀서가 한곳의 상태를 변경하는 것은 문제가 발생한다. 그렇기 때문에 안전한 데이터 변경을 위해서 애플리케이션 상태를 여러 개의 부분(리듀서)으로 분리한 후 하나의 스토어로 결합한다
- combinationReducers() : 여러개의 리듀서를 쉽게 결합한다.
2) src/modules/movies.js - 리듀서(액션/액션 생성자)
- Redux 에서 리듀서는 액션이 스토어에 전달될 때마다 실행되는 함수다.
- 리듀서의 첫 번째 인자 : state 는 전체 상태에서 해당 리듀서가 관리하는 일부분에 대한 참조
- 리듀서의 두 번째 인자 : action 은 스토어로 전달된 액션을 표현하는 객체
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | const {handleAction} = require( 'redux-actions' )
const FETCH_MOVIES = 'movies/FETCH_MOVIES'
const FETCH_MOVIE = 'movies/FETCH_MOVIE'
const initalState = {
movies:[],
movie:{}
}
module.exports = {
fetchMoviesActionCreator : (movies) => ({
type:FETCH_MOVIES,
movies
}),
fetchMovieActionCreator : (index) => ({
type:FETCH_MOVIE,
index
}),
reducer: handleAction({
[FETCH_MOVIES] : (state, action) => ({
...state,
all: action.movies
}),
[FETCH_MOVIE] : (state, action) => ({
...state,
current: state.all[action.index - 1]
})
}, initalState)
}
|
- handleActions : 키는 액션, 값은 함수인 맵 같은 형태의 객체를 받는다. 액션 종류에 따라 하나의 함수만을 호출 한다.
- switch/case 문보다는 위의 문장으로 작성해야한다.
a) 액션
- 스토어의 데이터를 변경하기 위해 액션을 사용한다.
- 액션의 실행은 store.dispatch() 또는 connect() 를 통해 실행된다. 해당 액션은 스토어에 전달된다.
1 2 3 4 | this .props.dispatch({
type:FETCH_MOVIE,
movie:{}
})
|
- type : 액션은 type 속성을 가지며 최근 개발 방식에 따르면 문자열 상수로 선언한다.
(const FETCH_MOVIES = 'movies/FETCH_MOVIES')
- 실행과정
ㄱ) 컴포넌트에서 type 속성과 필요한 데이터를 담은 액션 객체를 dispatch() 에 전달하여 실행한다
ㄴ) 리듀서 모듈에서 관련되어 있는 리듀서를 실행한다.
ㄷ) 스토어가 새로운 상태로 갱신되고 컴포넌트에서 새로운 상태를 전달 받는다.
b) 액션 생성자
- 스토어를 변경하려면 모든 리듀서에 액션을 전달해야한다.
- 리듀서는 액션의 type에 따라 애플리케이션 상태를 변경한다. 따라서 항상 액션의 type을 알아야한다는 번거로움이 있다.
- 액션 생성자를 이용하여 type을 감출 수 있다.
- 실행과정
ㄱ) 필요한 데이터와 함께 액션 생성자를 실해한다. 액션 생성자는 리듀서 모듈에서 정의할 수 있다.
ㄴ) 컴포넌트에서 스토어로 액션을 전달한다. 액션 type을 몰라도 실행 가능하다
ㄷ) 리듀서 모둘에서 관련된 리듀서를 실행한다.
ㄹ) 스토어가 새로운 상태로 갱신한다.
1 | this .props.dispatch(fetchMoviesActionCreator({movie:{}}))
|
4. 컴포넌트를 스토어 연결하기
- 컴포넌트 최상위 계층에 provider 를 둔다고 연결되지 않는다.
- 컴포넌트를 스토어에 연결하는 작업은 특정 컴포넌트를 위한 명시적인 선택사항이다.
- 컨테이너 컴포넌트는 스토어와 디스패처를 필요로하고 프레젠테이션 컴포넌트는 스토어가 필요없다.
- 스토어에 연결된 컴포넌트는 속성을 통해 스토어의 어느 데이터에도 접근 가능하다.
1 2 3 4 5 | const {connect} = require( 'react-redux' )
class Movies extends React.Component{
...
}
module.exports = connect()(Movies)
|
- connect() : react-redux 패키지의 일부이며 최대 네 개의 인자를 전달한다. connect() 는 함수를 반환하고 반환된 함수를 Movies컴포넌트에 적용한다. 결과적으로 Movies컴포넌트가 아닌 connect() 로 호출한 Movies 컴포넌트를 내보내게 되고, 상위 Provider 컴포넌트가 있으므로 Movies 컴포넌트가 스토어에 연결된다.
- Movies 컴포넌트는 스토어의 어느 데이터도 받고 액션도 전달 할 수 있다. 그러나 원하는 형태로 데이터를 받기위해선 간단한 맵핑 함수를 생성 해서 상태를 컴포넌트 속성으로 연결한다
1) 상태를 컴포넌트 속성으로 연결
- 맵핑 함수를 react-redux 의 connect() 메서드에 전달한다.
1 2 3 4 5 6 | module.exports = connect( function (state){
return state
})(Movies)
module.exports = connect(state => state)(Movies)
|
- 전체 애플리케이션 상태를 속성으로 전달 받음
a) Movies 컴포넌트가 필요한 부분만 받기
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Movies extends React.Component{
renser(){
const {
children,
movies = [].
params = {}
} = this .props
......
module.exports = connect(({movie})) => ({
movies: movies.all
}), {
fetchMoviesActionCreator
})(Movies)
|
- movies.all 만 받는다
5. 스토어에 액션 전달하기
1) dispatch()
- 액션을 인자로 받아 스토어에 전달하는 함수이다.
1 2 3 4 5 6 | componentWillMount(){
this .props.dispatch({
type:FETCH_MOVIE,
movie:{}
})
}
|
- 액션의 전달이 완료되어 스토어가 변경되면 스토어에 연결될 모든 컴포넌트 중에 애플리케이션 상태의 갱신 부분에 의존하는 컴포넌트들이 다시 렌더링 된다.
- type을 액션 생성자(fetchMovieActionCreator())로 대체한다
1 2 3 4 5 6 7 8 9 | const fetchMovieActionCreator = (response) => {
type:FETCH_MOVIE,
movie: response.data.data.movie
}
....
componentWillMount(){
...
this .props.dispatch(fetchMovieActionCreator(response))
}
|
ㄱ) 데이터를 비동기로 가져온다
ㄴ) 액션을 생성한다(fetchMovieActionCreator())
ㄷ) 액션을 전달한다(this.props.dispatch())
ㄹ) 리듀서를 실행한다
ㅁ) 속성을 새로운 상태로 갱신(this.props.movie)
6. 컴포넌트 속성으로 액션 생성자 전달하기
- 별도의 모듈에 액션 생성자를 정의하고 불러와서 컴포넌트 속성으로 추가한다.
- connect() 의 두번째 인자를 이용해서 액션 생성자를 메서드로 전달한다
1 2 3 4 5 6 7 8 9 10 11 | const{
fetchMoviesActionCreator
} = require( 'modules/movies.js' )
class Movies extends Component{
...
}
module.exports = connect(state =>({
movies:state.movies.all
}), {
fetchMoviesActionCreator
})(Movies)
|
- 속성을 통해 fetchMovieActionCreator() 를 참조할 수 있고 dispatch() 를 사용하지 않고 스토어에 액션을 전달 할 수 있다
1 2 3 4 5 6 | class Movies extends Component{
componentWillMount(){
this .props.fetchMoviesActionCreator()
}
....
}
|
- 액션 생성자는 자동으로 dispatch() 호출에 감싸지게 된다.
- 명확하게 하기 위해 이름 변경도 가능하다
1 2 3 4 5 6 7 8 9 10 11 12 13 | const{
fetchMoviesActionCreator
} = require( 'modulee/movies.js' )
class Movies extends Component{
componentWillMount(){
this .props.fetchMovies()
}
...
module.exports = connect(state => ({
movies : state.movies.all
}), {
fetchMovies: fetchMoviesActionCreator
})(Movies)
|