donaricano-btn

1. node 혹은 npm start 시 에러 발생

-  Something is already running on port 3000 


2. 해결방안

1) linux/mac

- $ lsof -i tcp:3000

- $ kill -i PID

2) window

- $ netstat -ano | findstr :3000

- $ tskill PID


블로그 이미지

리딩리드

,
donaricano-btn

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",
  },
  "author": "Azat Mardan (http://azat.co)",
  "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', //브라우저에 ES2015 환경을 완전히 갖추기 위한 폴리필
      './src/index.js'
    ]
  },
  output: {
    path: path.join(__dirname, 'build'), //path.join() 을 이용하여 경로를 잡는다
    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')//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}/> //빈 url 에 IndexRoute 정의한다.
            <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 // ES6/ES2015 해체할당을 이용해 ./movies.js의 reducer속성을 불러와서 리듀서 객체인 movies를 생성한다.
} = require('./moives')
 
module.exports = combineReducers({ //movies 가 포함된 결합된 리듀서를 내보낸다
    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) => ({ //액션 객체를 반환하는 FETCH_MOVIES액션 생성자를 정의한다
        type:FETCH_MOVIES,
        movies
    }),
    fetchMovieActionCreator : (index) => ({
        type:FETCH_MOVIE,
        index
    }),
    reducer: handleAction({
        [FETCH_MOVIES] : (state, action) => ({
            ...state,
            all: action.movies //Movies 컴포넌트에서 모든 영화 목록을 가져간다
        }),
        [FETCH_MOVIE] : (state, action) => ({
            ...state,
            current: state.all[action.index - 1] //Movie 컴포넌트에서 URL매개변수의 영화 id 에 따라 현재 영화를 가져간다
        })
    }, 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)
 
//ESnext  스타일
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(){
    ...//ajax 요청
  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 // modules/movies.js 에서 액션 생성자를 불러온다
} = require('modules/movies.js')
class Movies extends Component{
 ...
}
module.exports = connect(state =>({
  movies:state.movies.all //movies속성을 추가하기 위해 데이터를 연결한다
}), {
  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() //fetchMovies() 로 액션 전달
  }
...
module.exports = connect(state => ({
  movies : state.movies.all
}), {
  fetchMovies: fetchMoviesActionCreator //액션 메서드의 이름을 변경한다
})(Movies)

블로그 이미지

리딩리드

,
donaricano-btn

Redux 란?


1. 정의

- react 데이터 라이브러리의 하나로 가장많이 사용한다. 

- flux 아키텍처의 구현체 중 인기가 가장높다



2. 특징

1) 훌륭한 개발 생태계

2) 간결성

3) 훌륭한 개발자 경험 : 핫 리로딩과 시간 여행 디버깅을 할 수 있다.

4) 리듀서 구성: 최소한의 코드만으로 기능을 구형한다.

5) 서버측 렌더링 지원


3. React 앱에서 redux

- 스토어는 모든 데이터를 저장하고, 이 데이터를 조작할 수 있는 메서드를 제공한다. 스토어를 생성할 때는 createStore()  메서드를 사용한다

- Provider 컴포넌트는 모든 컴포넌트가 스토어에서 데이터를 가져올 수 있도록 만들어준다

- connect() 메서드는 컴포넌트를 감싸서 스토어에 있는 애플리케이션 상태의 일부를 컴포넌트의 속성에 연결한다.

블로그 이미지

리딩리드

,
donaricano-btn

브라우저 히스토리 vs 해시 히스토리



1. 해시 히스토리

- #기호를 사용하여 페이지를 다시 불러오지않고 탐색한다.

- router/#/posts/http2

- 대부분의 단일 페이지 애플리케이션이 해시를 사용하여 페이지를 완전히 새로고침 하지 않고 앱의 변경을 반영한다.

- 구형 브라우저 구현이 필요하다면 추천한다.

1) 사용법

1
2
3
4
5
6
7
8
9
10
11
12
//해시 히스토리 정의
const ReactRouter = require('react-router')
const History = require('history')
 
let hasHistory = ReactRouter.useRouterHistory(History.createHashHistory)({
  queryKey:false
})
<Router history={hashHistory}/>
 
//React Router 에 포함된 해시 히스토리 에서 사용
const {hashHistory} = require('react-router')
<Router history={hasHistory}/>

-  history를 초기화할 때 queryKey 를 false 로 설정하여 성가신 쿼리 스트링(?_k=vl8reh)방지한다

- queryKey 는 오래된 브라우저를 지원하고 탐색할 때 상태를 전달하기 위해 설정된 기본값이다.


2. 브라우저 히스토리

- 해시 히스토리는 url에 #값이 표시된다. 브라우저 히스토리는 이를 방지한다

- router/posts/http2

- 서버측의 설정이 필요하다.

- 브라우저 히스토리는 해시를 사용하지 않는 실제 url 를 사용하기 때문에 각각의 요청이 서버로 전송된다.  그렇기에 어떤 url 이든 같은 파일로 응답하도록 한다.

1) 사용법

1
2
const {browserHistory} = require('react-router')
<Router history={browserHistory}/>


블로그 이미지

리딩리드

,