"create-react-app ." 명령으로 react app 생성 후 배포를 위해서는 아래의 명령으로 buid 합니다.
npm run build
이렇게 하면 build 폴더가 생성되는데 필요한 파일을 서버가 작동하는 폴더에 위치시키면 됩니다.
2. httpd.conf 항목 추가
매번 build된 파일을 apache가 서비스하는 폴더로 옮기는 작업은 번거롭습니다. 현재 작업중인 폴더를 직접 서비스하도록 추가하고 싶다면, httpd.conf파일에 아래의 항목을 추가해줍니다.
Alias /react "D:/05_Program_dev/03_web/04_react_hello/build"
<Directory "D:/05_Program_dev/03_web/04_react_hello/build">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
이렇게 하면 'localhost/react'로 접속 시도시 해당 폴더에 접속하게 됩니다. 따라서 작업한 파일을 보려면
'localhost/react/index.html'로 시도하면 됩니다.
3. package.json homepage 수정
아직 apache 설정이 익숙치 않아서 이것저것 건드려봅니다. 어떤 앱은 2번까지만 수행해도 되고, 또다른 경우에는 여전히 흰 화면만 나오고 있습니다. 이것이 정답인지 알기는 힘들지만, 이럴땐 homepage속성에 접근하고자 하는 url을 입력했더니 수행 가능해졌습니다.
4. .htaccess 파일 추가??
.htaccess 파일을 생성하는 방법이 있습니다. 배포 폴더 내에 또는 Build하기 전 /publish 폴더 내에 .htaccess 파일을 추가하여 아래와 같이 작성해줍니다.
이 방법을 쓰려면 httpd.conf 파일의 AllowOverride 부분을 AllowOverride All로 해야합니다. 그래야 .htaccess의 접근을 허용해줍니다. 그러나 이 방법 성능 및 보안의 문제가 있다고 하여 사용을 권장하지는 않는 것 같습니다. (참고자료)
또..어떤 분은 아래와 같이 속성을 지정한다고 했는데 이건 저도 안해봤습니다. 이래저래 안될경우 따라해보려고 소스만 남겨놓습니다. 상황에 맞게 시도해보시기 바랍니다.
과제를 따라 수행하면서도 리덕스(Redux) 개념이 잘 안서는데, 나름 이해한 부분을 정리해봅니다.
리덕스는 store로부터 데이터를 직접 공급받기 위한 관리체계 개념입니다. 1개의 store에서 모든 데이터를 관리하죠. 동작 순서는 아래와 같으며, Action, Reducer를 별도 파일로 구성하고, Store도 App.js에서 생성해주어야 합니다.
1. Action
'액션 생성자'에서 Action에 대한정의만합니다. 단순히 어떤 동작(타입)인지만 정의합니다. 실제 'Action' 이라고 할 수 있는 '동작'의 개념은 Reducer가 수행합니다. 예로 아래와 같이 사용합니다. 이전 수행한 과제 블로그를 보시면 type 마져도 ActionType.js에 모아서 정의해 놓고 import해서 쓰기도 합니다.
import * as types from './ActionTypes'; // Action 타입만 별도로 정의할 경우..
export function setColor(color) {
return {
type: types.SET_COLOR,
color: color
};
}
export function decrement(){
return{
type: types.DECREMENT
};
}
export function setColor(color){
return {
type: types.SET_COLOR,
color
}
}
기본적으로 'type' 은 필수항목입니다. 그리고 추가 키가 있을 수 있는데(optional), payload라고도 하고, 본 과제에서는 인자로 color값을 받아들이기 위해 color라는 키를 사용했습니다.
2. Reducer
위에서 말한대로 실제 동작을 정의합니다. 즉, 실질적인 store의 state를 변화시키는 함수입니다. 단 몇가지 제약이 있습니다.
(1) 순수 함수이어야 합니다. class type으로 정의 불가능합니다.
(2) 받아들인 데이터를 변경할 수 없습니다. 이전 state와 action을 받아서 다음 state를 반환할 뿐, 이전 state를 변경할 수 없습니다. (previous State, Action) => newState
사용 방법의 예는 아래와 같습니다.
(1) initialState를 선언해서 컴포넌트의 인자료 전달해줍니다.
(2) switch문을 사용하여 action.type에 따라 각각의 변경을 수행합니다. (types가 아닌 type인 점 주의 필요)
import * as types from '../Actions/ActionTypes';
//초기상태 정하기
const initialState = {
number: 0, //최초 초기상태 정의
};
export default function counter(state = initialState, action) { // 초기값 입력이 undefined일 때 initialState 할당
switch (action.type) {
case types.INCREMENT:
return {
...state, // ...state에서 기존 정의한 state를 모두 복사해옴.
number: state.number + 1, //그 중 number에 새 값 설정
};
case types.DECREMENT:
return {
...state,
number: state.number - 1
};
default:
return state; // 기존 상태 그냥 전달. 이 세 케이스에 SET_COLOR가 없으므로 다른 reducer를 찾게 됨
}
}
3. Store
Store는 공용 데이터가 저장되는 공간입니다. Reducer가 처리하여 생성한 new state가 store에 반영됩니다.
(1) store 생성은 createStore(reducers) 함수로 하며, 인자로 reducer를 전달합니다.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore } from 'redux';
import reducers from './Reducers'; // index.js 파일 불러옴
import { Provider } from 'react-redux'; // react-redux의 Provider 임포트
const store = createStore(reducers); //스토어 생성시에는 reducer를 전달함
ReactDOM.render(
<React.StrictMode>
<Provider store={store}> {/*Provider의 props로 store를 선언 */}
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
4. View(컴포넌트) - Action 연결
(1) 컴포넌트와 redux와의 연결은 connect 함수로 합니다. 개념은, Redux와 컴포넌트를 연결하는 새로운 함수를 반환하는 것입니다. connect의 인자로는 mapStateToProps와 mapDispatchToProps가 있습니다.
(2) mapStateToProps는 현재 컴포넌트의 props와 store의 state를 연결합니다.
(3) mapDispatchToProps는 현재 컴포넌트의 props action과 리덕스의 액션( type 정의 부분)과 연결합니다.
import React, { Component } from 'react';
import Value from './Value';
import Control from './Control';
import { connect } from 'react-redux';
import * as actions from '../Actions';
class Counter extends Component {
constructor(props) {
super(props);
this.setRandomColor = this.setRandomColor.bind(this);
}
setRandomColor() {
const color = [
Math.floor((Math.random() * 55) + 200),
Math.floor((Math.random() * 55) + 200),
Math.floor((Math.random() * 55) + 200)
];
this.props.handleSetColor(color);
}
render() {
const color = this.props.color;
const style = {
background: `rgb(${color[0]}, ${color[1]}, ${color[2]})`
};
return (
<div style={style}>
<Value number={this.props.number} />
<Control
onPlus={this.props.handleIncrement}
onSubtract={this.props.handleDecrement}
onRandomizeColor={this.setRandomColor}
/>
</div>
);
}
}
const mapStateToProps = (state) => { // 컴포넌트의 state가 아닌 그냥 파라미터 이름(리덕스의 state 와 연결)
return {
number: state.counter.number, // number에 store state의 counter.number 연결
color: state.ui.color // number에 store state의 ui.color 연결
};
};
const mapDispatchToProps = (dispatch) => { //액션을 연결.
return {
handleIncrement: () => { dispatch(actions.increment()) },
handleDecrement: () => { dispatch(actions.decrement()) },
handleSetColor: (color) => { dispatch(actions.setColor(color)) }
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
// 컴포넌트를 redux에 연결하는 또다른 함수 반환하고, 반환된 함수의 인자로 Counter를 넣어준다.
정리하자면,
1. View
<Control> 컴포넌트의 onPlus 이벤트 발생 시 props의 handleIncrement가 실행되고,
2. Action
mapDispatchToProps에서 정의된 연결에 따라 dispatch(actions.increment()) 가 실행되면 Action에서
{ type: type.INCREMENT} 로 설정한다.
3. Reducer
type키의 값에 따라 INCREMENT일 경우 Reducer의 동작 정의에 따라 숫자 1이 더해지고, store의 state값을 갱신한다.
4. Store
Reducer에 의해 Store의 state값이 갱신되면 컴포넌트를 다시 렌더링 한다.
입니다. 좀 어렵습니다. 익숙해지는 건 더 어려울 것 같습니다. 그래도 힘내서 열심히 공부해보겠습니다^^
store에 연결 된 새로운 컴포넌트 클래스가 반환됨. 옵션이 없으면 this.props.store로 접근 가능
옵션
connect(
[mapStateToProps], //state 를 해당 컴포넌트의 props로 연결
[mapDispatchToProps], //dispatch한 함수를 props로 연결
[mergeProps], //state와 dispatch를 동시에 사용할 경우. 잘 사용되지 않음
[options] // pure=true: 불필요한 업데이트를 하지 않음, withRef=false: 잘 사용하지 않음
)
3. index.js 에서 Provide 컴포넌트 추가 (강좌 4와 동일)
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducers from './Reducers';
const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
4. components/Counter.js에서 작성
import React, { Component } from 'react';
import Value from './Value';
import Control from './Control';
import { connect } from 'react-redux';
import * as actions from '../Actions';
class Counter extends Component {
constructor(props) {
super(props);
this.setRandomColor = this.setRandomColor.bind(this);
}
setRandomColor() {
const color = [
Math.floor((Math.random() * 55) + 200),
Math.floor((Math.random() * 55) + 200),
Math.floor((Math.random() * 55) + 200)
];
this.props.handleSetColor(color);
}
render() {
const color = this.props.color;
const style = {
background: `rgb(${color[0]}, ${color[1]}, ${color[2]})`
};
return (
<div style={style}>
<Value number={this.props.number} />
<Control
onPlus={this.props.handleIncrement}
onSubtract={this.props.handleDecrement}
onRandomizeColor={this.setRandomColor}
/>
</div>
);
}
}
const mapStateToProps = (state) => { // 컴포넌트의 state가 아닌 그냥 파라미터 이름(리덕스의 state 와 연결)
return {
number: state.counter.number, // number에 store state의 counter.number 연결
color: state.ui.color // number에 store state의 ui.color 연결
};
};
const mapDispatchToProps = (dispatch) => { //액션을 연결.
return {
handleIncrement: () => { dispatch(actions.increment()) },
handleDecrement: () => { dispatch(actions.decrement()) },
handleSetColor: (color) => { dispatch(actions.setColor(color)) }
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter); // 컴포넌트를 redux에 연결하는 또다른 함수 반환
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducers from './Reducers';
const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
// 스토어 생성 및 redux-devtools-extension을 스토어에 추가(활성화)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import * as types from '../Actions/ActionTypes';
//초기상태 정하기
const initialState = {
number: 0, //최초 초기상태 정의
};
export default function counter(state = initialState, action) { // 초기값 입력이 undefined일 때 initialState 할당
switch (action.type) {
case types.INCREMENT:
return {
...state, // ...state에서 기존 정의한 state를 모두 복사해옴.
number: state.number + 1, //그 중 number에 새 값 설정
};
case types.DECREMENT:
return {
...state,
number: state.number - 1
};
default:
return state; // 기존 상태 그냥 전달. 이 세 케이스에 SET_COLOR가 없으므로 다른 reducer를 찾게 됨
}
}
2. ui.js
import * as types from '../Actions/ActionTypes';
const initialState = {
color: [255, 255, 255]
};
export default function ui(state = initialState, action) {
if (action.type === types.SET_COLOR) {
return {
color: action.color
}
} else {
return state;
}
}
3. index.js
//reducer 합치는 파일
import { combineReducers } from 'redux';
import counter from './Counter';
import ui from './ui';
const reducers = combineReducers({
counter, ui
});
export default reducers;