스터디/KAKAOCLOUDSCHOOL

[React] - 개발자 지망생 스터디 - 29일차(1)

shineIT 2022. 12. 12. 10:11

8. redux 실습

1) 프로젝트 생성 및 필요한 라이브러리 설치

$yarn create react-app redux
$cd redux
$yarn add redux react-redux

2) UI 작업

# Counter 와 ToDo 배치
> Components 디렉토리 생성
🗂redux -> 📁 src -> (CREATE) 📁 components
> components 디렉토리에 카운터를 위한 Counter.jsx 파일 생성
🗂redux -> 📁 src -> 📁 components -> (CREATE) 📄 Counter.jsx
import React from "react";

const Counter = ({number, onIncrease, onDecrease}) => {
    return (<div>
        <h1>{number}</h1>
        <div>
            <button onClick={onIncrease}>+1</button>
            <button onClick={onDecrease}>-1</button>
        </div>
    </div>);
}

export default Counter;​

> components 디렉토리에 ToDos.jsx 파일을 생성하고 작성
🗂redux -> 📁 src -> 📁 components -> (CREATE) 📄 ToDos.jsx
import React from "react";
//하나의 항목을 출력하기 위한 컴포넌트
const ToDoItem = ({todo, onToggle, onRemove}) => {
    return (<div>
        <input type = "checkbox"/>
        <span>텍스트</span>
        <button>삭제</button>
    </div>);
}

//여러 개의 ToDoItem을 출력할 컴포넌트
const ToDos = ({input, todos, onChangeInput, onInsert, onToggle, onRemove,}) => {
    const onSubmit = (e) => {
        e.preventDefault();
    };
    return (
        <div>
            <form onSubmit={onSubmit}>
                <input/>
                <button type="submit">등록</button>
            </form>
            <div>
                <ToDoItem />
                <ToDoItem />
                <ToDoItem />
                <ToDoItem />
                <ToDoItem />
            </div>
        </div>
    );
}
export default ToDos;

> App.js 파일 수정
🗂redux -> 📁 src -> 📄 App.js
import React from "react";
import Counter from "./components/Counter";
import ToDos from "./components/ToDos";

function App() {
  return (
    <div>
      <Counter number={0}/>
      <hr/>
      <ToDos/>
    </div>
  );
}
export default App;​

3) redux 관련 모듈 생성

> modules 디렉토리 생성
🗂redux -> 📁 src -> (CREATE) 📁 modules
> counter.js 파일을 생성
🗂redux -> 📁 src -> 📁 modules -> (CREATE) 📄 counter.js
//액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

//액션 생성 함수 정의
export const increase = () => ({type:INCREASE});
export const decrease = () => ({type:DECREASE});

//초기 상태를 정의
const initialState = {
    number:0
}

//리듀서 함수 정의
const counter = (state=initialState, action) => {
    switch( action.type){
        case INCREASE:
            return {number:state.number + 1};
        case DECREASE:
            return {number:state.number - 1};
        default:
            return state;
    }
};
export default counter;​

 

> todos.js 파일을 생성
🗂redux -> 📁 src -> 📁 modules -> (CREATE) 📄 todos.js
//액션 타입 정의
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

//액션 생성 함수 정의
export const changeInput = input => ({
    type:CHANGE_INPUT,
    input
});

let id = 3; //샘플 데이터를 2개 삽입할 것이기 때문에 3을 지정
export const insert = text => ({
    type:INSERT,
    todo:{
        id:id++,
        text,
        done:false
    }
});
export const toggle = id => ({
    type:TOGGLE,
    id
});
export const remove = id => ({
    type:REMOVE,
    id
});

//초기 상태 정의
const initialState = {
    input: '',
    todos: [{
        id: 1,
        text: 'Node',
        done: true,
    }, {
        id: 2,
        text: 'React',
        done: false,
    }]
};

// 리듀스 함수 정의
const todos = (state=initialState, action) => {
    switch( action.type){
        case CHANGE_INPUT:
            return {...state, input:action.input};
        case INSERT:
            return {...state, todos:state.todos.concat(action.todo)};
        case TOGGLE:
            return {...state, todos:state.todos.map(
                todo => todo.id === action.id ? {...todo,done:!todo.done}:todo)};
        case REMOVE:
            return {...state, todos:state.todos.filter(todo => todo.id !== action.id)};
        default:
            return state;
    }
}
export default todos;

4) redux 관련 모듈 작성

> modules 디렉토리 안에 index.js 파일을 생성하고 작성
🗂redux -> 📁 src -> 📁 modules -> (CREATE) 📄 index.js
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

const rootReducer = combineReducers({
    counter,
    todos,
});

export default rootReducer;


> index.js 수정
🗂redux -> 📁 src ->  📄 index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

//추가 부분 (line 8 ~ 11)
import { legacy_createStore as createStore } from 'redux';
import {Provider} from 'react-redux';
import rootReducer from './modules';
const store = createStore(rootReducer);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
//변경 부분 (line 16 ~ 18)
  <Provider store={store}>
    <App />
  </Provider>
);

// 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) 컨테이너 컴포넌트 작업

> 컨테이너 컴포넌트 : redux를 사용하는 컴포넌트
> src 디렉토리에 containers 디렉토리 생성
🗂redux -> 📁 src -> (CREATE) 📁 containers

#컨테이너 컴포넌트 생성
> react-redux 의 connect 함수 사용
   connet(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
☑︎ mapStateToProps는 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해서 생성하는 함수이고
☑︎ mapDispatchToProps는 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수

> containers 디렉토리에 CounterContainer.jsx 파일을 생성하고 작성
🗂redux -> 📁 src -> 📁 containers -> (CREATE) 📄 CounterContainer.jsx
import React from 'react';
import {connect} from 'react-redux';
import Counter from '../components/Counter';
import {increase, decrease} from '../modules/counter';

const CounterContainer = ({number, increase, decrease}) => {
    return (
        <Counter number={number} 
        onIncrease={increase} onDecrease={decrease} />
    );
};

const mapStateToProps = state => ({
    number: state.counter.number
});

const mapDispatchToProps = dispatch => ({
    increase: () => {dispatch(increase());},
    decrease: () => {dispatch(decrease());}
})

export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);


> App.js 수정
🗂redux -> 📁 src -> 📄 App.js

import React from "react";
import CounterContainer from "./containers/CounterContainer";
import ToDos from "./components/ToDos";

function App() {
  return (
    <div>
      <CounterContainer/>
      <hr/>
      <ToDos/>
    </div>
  );
}
export default App;

 

> ToDos를 위한 ToDosContainer.jsx 파일을 생성하고 작성
🗂redux -> 📁 src -> 📁 containers -> (CREATE) 📄 ToDosContainer.jsx
import React from "react";
import {connect} from 'react-redux';
import {changeInput, insert, toggle, remove} from '../modules/todos';
import ToDos from "../components/ToDos";

const ToDosContainer = ({input, todos, changeInput, insert,remove}) => {
    return (<ToDos input={input} todos={todos} onChangeInput={changeInput}
        onInsert={insert} onToggle={toggle} onRemove={remove}/>);
}

export default connect(({todos}) => ({
        input:todos.input,
        todos:todos.todos
    }), {changeInput,insert,toggle,remove}
)(ToDosContainer);

> App.js 수정 (ToDos -> ToDosContainer)
🗂redux -> 📁 src -> 📄 App.js
import React from "react";
import CounterContainer from "./containers/CounterContainer";
import ToDosContainer from './containers/ToDosContainer';

function App() {
  return (
    <div>
      <CounterContainer/>
      <hr/>
      <ToDosContainer/>
    </div>
  );
}
export default App;

> components 디렉토리의 ToDos.jsx 수정
🗂redux -> 📁 src -> 📁 components ->📄 ToDos.jsx
import React from "react";
//하나의 항목을 출력하기 위한 컴포넌트
const ToDoItem = ({todo, onToggle, onRemove}) => {
    return (<div>
        <input type = "checkbox" 
        onClick={()=>onToggle(todo.id)}
        checked={todo.done}
        readOnly={true}/>
        <span style={{textDecoration:todo.done? 'line-through':'none'}}>
            {todo.text}
        </span>
        <button onClick={()=>onRemove(todo.id)}>삭제</button>
    </div>);
}

//여러 개의 ToDoItem을 출력할 컴포넌트
const ToDos = ({input, todos, onChangeInput, onInsert, onToggle, onRemove,}) => {
    const onSubmit = (e) => {
        e.preventDefault();
        onInsert(input);
        onChangeInput('');
    };
    const onChange = (e) => onChangeInput(e.target.value);
    return (
        <div>
            <form onSubmit={onSubmit}>
                <input value={input} onChange={onChange}/>
                <button type="submit">등록</button>
            </form>
            <div>
                {todos.map(todo => (
                    <ToDoItem
                    todo={todo}
                    key={todo.id}
                    onToggle={onToggle}
                    onRemove={onRemove}/>
                ))}
            </div>
        </div>
    );
}
export default ToDos;

9. MiddleWare

1) 개요

> 액션이 디스패치 된 후 리듀서에서 해당 액션을 받아서 작업을 수행하기 전이나 후에 추가적인 작업을 할 수 있도록 해주는 것
> 작업을 수행하기 전에는 유효성 검사 같은 작업을 많이 하고 작업이 수행된 후에는 로그 기록을 많이 함.
> 유사한 용도로 사용되는 것들을 부르는 명칭으로는 Filter, Interceptor, AOP 등이 있음

2) 직접 생성한 미들웨어 적용

> src 디렉토리에 middlewares 디렉토리 생성
🗂redux -> 📁 src -> (CREATE) 📁 middlewares
> middlewares 디렉토리에 미들웨어로 사용할 mymiddleware.js 파일 만들고 작성
🗂redux -> 📁 src -> 📁 middlewares -> (CREATE) 📄 mymiddleware.js
const mymiddleware = store => next => action => {
    //동작을 로깅
    console.log(action);
    //다음 미들웨어나 리듀서에게 전달
    const result = next(action)
    //작업이 끝난 후 확인
    console.log(store.getStore());
    return result;
}
export default mymiddleware;

> src 디렉토리의 index.js 파일 수정
🗂redux -> 📁 src -> 📄 index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
import {Provider} from 'react-redux';
import rootReducer from './modules';
import mymiddleware from './middlewares/mymiddleware';

const store = createStore(rootReducer, applyMiddleware(mymiddleware));

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

// 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();​

3) 외부 라이브러리를 이용한 로그 기록

> 설치 : $yarn add redux-logger
> src 디렉토리의 index.js 파일 수정 (+logger)
🗂redux -> 📁 src -> 📄 index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { legacy_createStore as createStore, applyMiddleware } from 'redux';
import {Provider} from 'react-redux';
import rootReducer from './modules';
import mymiddleware from './middlewares/mymiddleware';
import logger from 'redux-logger';

const store = createStore(rootReducer, applyMiddleware(mymiddleware, logger));

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

// 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();

 

가상의 API Server 만들기

# 설치
> $npm install --location=global json-server
> $yarn global add json-server

# 프로젝트 루트 디렉토리에 가상의 데이터를 작성 - data.json

{
    "posts":[{
        "id":1,
        "content":"React",
        "done":true
    },{
        "id":2,
        "content":"Node",
        "done":false
    }]
}​

 

#서버 실행 :
> $npx json-server ./data.json --port 4000

# 브라우저에서 확인

> localhost:4000/posts
> localhost:4000/posts/1

# react 프로젝트에서 외부 서버의 데이터를 proxy를 통해서 가져오기
> package.json 파일을 열어서 "proxy":"http://localhost:4000" 추가 - 서버의 도메인을 작성해야 함
🗂 redux -> 📄 package.json
{[
		...
	],[
    		...
	]
	},
	"proxy":"http://localhost:4000"
}

> 요청을 수정
http://localhost:4000/posts → /posts
httpl://localhost:4000/posts/1 → /posts/1