TIL 22-05-03 (2)

thisisyjin·2022년 5월 3일
0

TIL 📝

목록 보기
38/113

React.js

Today I Learned ... react.js

🙋‍♂️ Reference Book

🙋‍ My Dev Blog


리액트를 다루는 기술 DAY 17

  • React + Redux

React + Redux Project

  • React에서 소규모 프로젝트에서는 state로도 충분하지만, 프로젝트 규모가 커지면 state 관리가 번거로움.
  • React에서 Redux를 사용하면, 상태 업데이트에 관한 로직모듈로 따로 분리하여 별개의 파일로 관리할 수 있음.
    -> 코드 유지보수 및 최적화 용이.
  • 여러 컴포넌트에서 동일한 상태를 공유해야할 때 매우 유용.

이전 포스팅에서 vanila JS 환경에서 리덕스를 사용할 때에는 store의 내장 함수인 store.dispatch, store.subscribe를 사용했음.

  • 리액트에서 리덕스를 사용할 때는, store객체를 직접 사용하기보다는
    react-redux라는 라이브러리의 유틸 함수컴포넌트를 사용함.

redux, react-redux 설치

$ yarn add redux react-redux

UI 준비하기

리액트 환경에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은
프레젠테이셔널 컴포넌트컨테이너 컴포넌트를 분리하는 것.

프레젠테이셔널 컴포넌트컨테이너 컴포넌트
state 관리 X. 그저 props를 받아 화면에 UI를 보여주기만 함리덕스와 연동. 리덕스로부터 state를 받아오기도 하고,
store에 액션을 디스패치 하기도 함.

-> 이러한 패턴을 사용하면 코드 재사용성도 높아지고, UI 작성에 더 집중할 수 있다.

UI 관련 프레젠테이셔널 컴포넌트src/components에 저장하고,
Redux에 연동된 컨테이너 컴포넌트src/containers 컴포넌트에 작성함.


1) Counter 컴포넌트 생성

src/components/Counter.js

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;

App.js

import Counter from "./components/Counter";

function App() {
  return (
    <div>
      <Counter number={0} />
    </div>
  );
}

export default App;

2) Todos 컴포넌트

const TodoItem = ({ todo, onToggle, onRemove }) => {
    return (
        <div>
            <input type="checkbox" />
            <span>예제 텍스트</span>
            <button>삭제</button>
        </div>
    );
};

const Todos = ({
    input,
    todos,
    onChangeInput,
    onInsert,
    onToggle,
    onRemove
}) => {
    const onSubmit = e => {
        e.preventDefalt();
    };
    return (
        <div>
            <form onSubmit={onSubmit}>
                <input />
                <button type="submit">등록</button>
            </form>
            <div>
                <TodoItem />
                <TodoItem />
                <TodoItem />
                <TodoItem />
                <TodoItem />
            </div>
        </div>
    );
};

export default Todos;
  • 파일 하나에 두 컴포넌트를 선언함. (분리해도 OK)

3) App.js 렌더링

import Counter from "./components/Counter";
import Todos from "./components/Todos";

function App() {
  return (
    <div>
      <Counter number={0} />
      <hr />
      <Todos />
    </div>
  );
}

export default App;

리덕스 관련 코드 작성

  • 리덕스를 사용할 때는 액션 타입, 액션 생성함수, reducer 코드를 작성해야 함.
  • 각각 다른 폴더를 지정하여 작성할 수도 있음.

  • Redux 공식 문서에서도 사용되는 기본적 구조.
  • actions, constants, reducers 세 개의 디렉터리를 만들고 그 안에 기능별로 파일을 생성.
    -> 새로운 액션을 만들 때 마다 세종류의 파일을 모두 수정해야하는 번거로움이 O.

🐥 Ducks 패턴

  • 아래와 같이 액션타입 / 액션 생성함수 / 리듀서 함수를 기능별로 파일 하나로 몰아서 작성함.
  • 둘중 아무 패턴이나 사용해도 됨. (상관 X)

counter 모듈 작성

1) 액션 타입 + 생성 함수

// 액션 타입 
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
  • 액션 타입은 대문자로 정의하고, 내용은 '모듈이름/액션이름'의 형태로 작성함.
    -> 다른 모듈과 액션이름이 중복되지 않도록.

  • 액션 생성함수는 export해줌.

2) initialState + reducer 생성

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;
  • initialState 값을 설정해준 후,
  • reducer 함수를 생성. (함수명은 구분을 위해 'counter'로 함.)
    -> export default 키워드로 함수를 export함.

✅ export vs. export default

  • export는 여러개를 내보낼 수 있지만
  • export default는 한개만 내보낼 수 있음.
  • import시에는 import counter로 한번에 불러올 수 있고,
    increase, decrease는 import {increase, decrease}로 불러와야함.

todos 모듈 작성

1) 액션 타입 + 생성 함수

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;

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
})

id는 전역변수 id로 관리해서 todo의 id로 사용하도록 함.
-> insert 함수가 호출될 때 마다 1씩 증가하도록. (id++)

2) initialState + reducer 생성

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;

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: '리덕스 기초 배우기',
            done: true
        },
        {
            id: 2,
            text: '리액트와 리덕스 사용하기',
            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;

루트 리듀서 생성

modules/index.js

import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";

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

export default rootReducer;

redux 라이브러리의 combineReducers함수를 이용하여 두개의 모듈을 합침.
-> createStore을 통해 스토어를 만들 때 하나의 리듀서를 사용해야 하기 때문.


리액트 어플리케이션에 리덕스 적용

  • redux 라이브러리의 createStore() 함수를 임포트함.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { createStore } from 'redux';
import App from './App';
import './index.css'
import rootReducer from './modules';

const store = createStore(rootReducer);

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

❗️ 루트 리듀서의 파일명을 index.js로 해주면, import시 폴더명까지만 적어도 된다!

import rootReducer from './modules';

Provider 컴포넌트 사용

  • react-redux의 Provider 컴포넌트를 임포트함.
  • Provider 컴포넌트 사용시 storeprops로 전달해주어야 함.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './App';
import './index.css'
import rootReducer from './modules';

const store = createStore(rootReducer);

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

🙋‍♀️ 참고 - 이제는 createStore() 이 아닌 react-reduxconfigureStore({}) 을 이용하자.


Redux DevTool

  • 크롬 확장프로그램 Redux DevTool로 설치한 후,
    아래와 같이 리덕스 스토어를 만드는 과정에서 적용 가능.
const store = createStore(
  rootReducer, /* preloadedState */
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
  • 위 코드를 적용하는 것 보다는 패키지를 설치하면 훨씬 간편하게 사용 가능.

🛠 redux-devtools-extension 설치

$ yarn add redux-devtools-extension

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import App from './App';
import './index.css'
import rootReducer from './modules';

const store = createStore(rootReducer, composeWithDevTools);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
  • Redux DevTool의 'state'탭을 살펴보면 아래와 같이 나온다.

🔻 참고로, 이렇게 차트형 or Raw(객체형식)으로도 볼 수 있다.


컨테이너 컴포넌트 생성

  • 리덕스 스토어에 접근하여 원하는 상태를 받아오고, 액션도 디스패치해줌.
  • 리덕스 스토어와 연동된 컴포넌트를 컨테이너 컴포넌트라고 한다.

1) CounterContainer 생성

src/containers/CounterContainer.js

import Counter from "../components/Counter";

const CounterContainer = () => {
    return <Counter />;
};

export default CounterContainer; 

위 CounterContainer 컴포넌트를 리덕스와 연동하기 위해서는 react-redux 라이브러리의 connect함수를 이용해야함.

ex>

connect(mapStateToProps, mapDispatchToPRops)(CounterContainer);

connect(mapStateToProps, mapDispatchToProps)()

CounterContainer.js (수정)

import Counter from "../components/Counter";
import { connect } from "react-redux";

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

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

const mapDispatchToProps = dispatch => ({
    increase: () => {
        console.log('increase');
    },
    decrease: () => {
        console.log('decrease');
    },
});

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

mapStateToProps와 mapDispatchToProps에서 리턴하는 객체 내부의 값은 컴포넌트의 props로 전달됨.

  • mapStateToProps : state를 인자로 받아옴. = 현재 store가 지니고 있는 값.
  • mapDispatchToProps : store.dispatch() 함수를 파라미터로 받아옴.

App - Container 렌더링

App.js (수정)

import CounterContainer from "./containers/CounterContainer";
import Todos from "./components/Todos";

function App() {
  return (
    <div>
      <CounterContainer />
      <hr />
      <Todos />
    </div>
  );
}

export default App;

-> Counter 대신 CounterContainer 컴포넌트 렌더링.

increase, decrease 기능 구현

CounterContainer.js

import Counter from "../components/Counter";
import { connect } from "react-redux";
import { increase, decrease } from "../modules/counter";

...

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

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

이제 +1, -1 버튼을 클릭하면 카운터가 변경된다.
Redux DevTool을 보면 아래와 같이 액션명이 나온다.

  • connect 함수 사용시, 일반적으로 위와 같이 mapStateToProps, mapDispatchToProps를 미리 정의해놓고 사용한다.
  • connect 안에 익명함수 형식으로 선언해도 괜찮다.
export default connect(state => ({
    number: state.counter.number,
}), dispatch => ({
    increase: () => dispatch(increase()),
    decrease: () => dispatch(decrease()),
})
)(CounterContainer);

bindActionCreator

  • 컴포넌트에서 액션을 dispatch로 감싸는 작업이 번거로움.
    액션 생성 함수가 많아질수록 더 번거로워짐.

  • redux 라이브러리에서 제공하는 유틸함수인 bindActionCreator을 사용해주면 됨.

CreateContainer.js

...
export default connect(state => ({
    number: state.counter.number,
}),
    dispatch =>
        bindActionCreators(
            {
                increase,
                decrease,
            },
            dispatch,
        )
)(CounterContainer);

dispatch => bindActionCreators({increase,decrease}, dispatch})() 해줌.
-> 따로 Action을 불러오고 dispatch로 감싸줄 필요가 없어짐.

mapDispatchToProps - 쉬운 방법

  • mapDispatchToProps()의 인자에 액션 생성함수로 이루어진 객체를 넣어줌.
    -> 액션 생성함수를 요소로 갖는 객체.
...
export default connect(state => ({
    number: state.counter.number,
}),
{
    increase, decrease
}
)(CounterContainer);

2) TodosContainer 생성

  • connect 함수를 이용하고, mapDispatchToProps를 함수가 담긴 객체로 나타냄
import { connect } from "react-redux";
import { changeInput, insert, toggle, remove } from "../modules/todos";
import Todos from "../components/Todos";

const TodoContainer = ({ input, todos, changeInput, insert, toggle, remove }) => {

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

export default connect(
    // state.todos 가져옴
    ({ todos }) => ({
        input: todos.input,
        todos: todos.todos,
    }),
    {
        changeInput,
        insert,
        toggle,
        remove
    },
)(TodoContainer);

아까 connect의 두번째 인자인 mapDispatchToProps함수의 인자로 state가 갔었는데,
이 함수는 state를(=store가 가지고 있는 값) 인자로 가짐.
여기서도 마찬가지로 state를 가지지만, state.todos를 {todos}로 구조분해 할당해줌.

App - Container 렌더링

import CounterContainer from "./containers/CounterContainer";
import TodosContainer from "./containers/TodosContainer";

function App() {
  return (
    <div>
      <CounterContainer />
      <hr />
      <TodosContainer />
    </div>
  );
}

export default App;

Todos.js - props 사용

components/Todos.js

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>
    );
};

const Todos = ({
    input,
    todos,
    onChangeInput,
    onInsert,
    onToggle,
    onRemove
}) => {
    const onSubmit = e => {
        e.preventDefalt();
        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;

RESULT

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글