작업환경설정
$ mkdir react-redux-tutorial
$ cd react-redux-tutorial
$ npm init vite
$ npm install
$ npm install redux react-redux
(@reduxjs/toolkit은 사용안하는지 궁금하닷.. )
UI준비
리액트 프로젝트에서 리덕스를 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트
와 컨테이터 컴포넌트
를 분리하는 것.
프레젠테이셔널 컴포넌트: 주로 상태관리가 이루어지지 않고 props만 받아와서 화면에 UI를 보여주기만 하는 컴포넌트. UI관련 컴포넌트.
src/components 경로컨테이너 컴포넌트: 리덕스와 연동되어 있는 컴포넌트. 리덕스로부터 상태를 받아오기도 하고 리덕스 스토어에 액션을 디스패치 하기도 함.
src/containers 경로
-> 이런 패턴이 필수는 아니지만 이 패턴을 사용하면 코드의 재사용성도 높아지고 관심사의 분리가 이루어져 UI를 작성할때 좀더 집중할 수 있다.
카운터 컴포넌트 만들기
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
이걸 작성하다가 궁금한점이 생겼다.onClick={onIncrease()}
를 하지 않는 이유는 뭘까?
ChatGPT씨에게 물어보았다ㅎㅎ
onClick={onIncrease()}
와 onClick={onDecrease()}
를 사용하는 것은 함수를 호출하려는 시도를 나타냅니다. 그러나 일반적으로 React에서는 함수를 직접 호출하는 것이 아니라, 이벤트 핸들러로 함수 참조(reference)를 전달해야 합니다.
만약 onClick={onIncrease()}를 사용하면, 컴포넌트가 렌더링될 때마다 onIncrease 함수가 즉시 호출됩니다. 이는 원하는 동작이 아니며, 실제로 이벤트가 발생할 때 함수를 실행하려면 이벤트 핸들러에 함수 참조만 전달해야 합니다.
따라서 올바른 방법은 다음과 같이 함수 참조를 전달하는 것입니다:
<button onClick={onIncrease}>+1</button> <button onClick={onDecrease}>-1</button>
예를들어 필터 체크박스가 있다. 체크박스를 체크하면 filter값이 넘어가고 setFilter를 한다.
체크박스에는 이런 이벤트가 걸려있다.onChange={(e) => handleFilter(e)}
이것도 함수 호출이 아닌 React 이벤트 핸들러에 함수참조를 전달하는것이다. onChange={handleFilter(e)}
로 작성하면 handleFilter가 즉시 호출되어버리게 된다. 이벤트 핸들러에는 함수를 전달해야하므로 괄호를 사용하지 않아야한다. . .
따라서 handleFilter를 호출하지 않고 함수 참조를 전달하는 것이 올바른 방법이다.
그러면 onClick={() => handleLikeClick()}이나 onClick={handleLikeClick}의 차이는 멀까?
onClick={() => handleLikeClick()}와 onClick={handleLikeClick}는 기능적으로 큰 차이가 없습니다. 둘 다 클릭 이벤트가 발생했을 때 handleLikeClick 함수를 호출합니다. 그러나 작은 차이점이 있습니다.
- 렌더링 시간에 함수 생성 여부:
onClick={() => handleLikeClick()}
: 이 코드는 매번 렌더링될 때마다 새로운 인라인 함수를 생성합니다. 따라서 부모 컴포넌트가 리렌더링되면 매번 새로운 함수가 생성되어 성능에 영향을 줄 수 있습니다. 하지만 일반적으로 이 차이는 성능상 큰 문제가 되지 않습니다.onClick={handleLikeClick}
: 이 코드는 렌더링 시간에 새로운 함수를 생성하지 않습니다. 그러므로 함수가 재생성되지 않기 때문에 불필요한 리렌더링을 방지하는 데 도움이 될 수 있습니다.
- 이벤트 핸들러 내에서 this 값:
- 만약 클래스 컴포넌트에서 이벤트 핸들러를 사용한다면,
onClick={() => handleLikeClick()}
를 사용해야 합니다. 이는 화살표 함수를 사용하여 함수를 감싸기 때문에 함수 내에서 this 값이 컴포넌트 자체를 가리킵니다. 반면에onClick={handleLikeClick}
는 이벤트 핸들러에서 this 값을 바인딩하지 않으므로, 클래스 메서드 내에서 this를 사용할 경우 문제가 발생할 수 있습니다.
최근 React 버전에서는 함수 컴포넌트에서 성능상의 이슈가 크게 나타나지 않기 때문에, 함수 컴포넌트에서는 두 가지 방식 중 편한 방식을 사용하는 것이 일반적입니다. 함수 컴포넌트에서는 주로 onClick={handleLikeClick}를 사용하며, 클래스 컴포넌트에서는 onClick={() => handleLikeClick()}를 사용하는 것이 일반적입니다.
이벤트 핸들러에 매개변수가 들어가면 onClick={() => handleLikeClick()}
로 작성해야한다고한다.. 왜냐면 handleLikeClick(props)
로 작성하면 함수 호출이 되는거니까(당연함)
이렇게 간단한걸 이제 배운다...
할일 목록 컴포넌트 만들기
TodoItem.jsx
import React from 'react' const TodoItem = ({ todo, onToggle, onRemove }) => { return ( <div> <input type="checkbox" /> <span>예제 테스트</span> <button>삭제</button> </div> ) } export default TodoItem
Todos.jsx
import React from 'react' import TodoItem from './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
컴포넌트들이 받아오는 props는 나중에 사용.
App.jsx
import Counter from './components/Counter' import Todos from './components/Todos' function App() { return ( <> <Counter number={0} /> <hr /> <Todos /> </> ) } export default App
App 컴포넌트에 렌더링해준다.
리덕스 관련 코드 작성
리덕스 관련 디렉터리 구조는 정해진 방법은 없지만 일반적으로 두가지 방법이 주로 사용된다.
일반적인 구조
src/actions
src/constants
src/reducers
actions, constants, reducers라는 세개의 디렉터리 만들고 각 기능별로 파일을 하나씩 만드는 방식. 코드 종류에 따라 다른 파일에 작성하여 편리하지만, 새로운 액션을 한번 만들때마다 세 종류의 파일을 모두 수정해야 하기때문에 불편할 수 있음.Ducks패턴
src/modules/(counter.js, todos.js ... )
액션 타입, 액션 생성함수, 리듀서 함수를 기능별로 파일 하낭네 몰아서 작성하는 방식. 일반적이 구조로 리덕스를 사용하다가 불편함을 느낀 개발자들이 자주 사용.
counter 모듈
// 액션 타입 // 액션이름은 대문자 // 문자열 내용 : 모듈이름/액션이름 const INCREASE = 'counter/INCREASE'; const DECREASE = 'counter/DECREASE'; // 액션 생성 함수 export const increase = () => ({ type: INCREASE }); export const decrease = () => ({ type: DECREASE }); const initialState = { number: 0 }; function 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;
export와 export default의 차이점
export는 여러번 쓸 수 있지만 export default는 한파일에서 하나만 가능.export default counter
는 import할때 import counter from ./counter
export const increase
는 import할때 import { increase } from ./counter
todos 모듈
// 액션 타입 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 }, ] } // 리듀서 function 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;
객체에 한개 이상의 값이 들어가기때문에 불변성 유지에 유의하면서 작성해야 한다.
spread연산자를 사용하고, 배열에 변화를 줄때는 배열 내장 함수를 사용하면된다.
루트 리듀서
리듀서를 여러개 만들었는데 createStore하여 스토어를 만들때는 리듀서를 하나만 사용해야 하므로
기조에 만들었던 리듀서들을 하나로 합쳐주어야한다. combineReducers
라는 유틸함수를 사용하여 쉽게 처리할 수 있다.
modules/index.js
import { combineReducers } from 'redux' import counter from './counter'; import todos from './todos'; const rootReducer = combineReducers({ counter, todos }); export default rootReducer;
리액트 애플리케이션에 리덕스 적용
스토어 생성, Provider와 리덕스 적용
main.jsx
import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' import rootReducer from './modules/index.js' import { legacy_createStore as createStore } from 'redux' import { Provider } from 'react-redux' const store = createStore(rootReducer); ReactDOM.createRoot(document.getElementById('root')).render( <Provider store={store}> <App /> </Provider> )
Redux DevTools의 설치 및 적용
리덕스 개발자 도구는 크롬확장프로그램으로 설치하여 사용할 수 있다.
리덕스 스토어 생성할때 다음과 같이 적용 가능하다.
const store = createStore( rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__&& window.__REDUX_DEVTOOLS_EXTENSION__() );
하지만 패키지를 설치하여 적용하면 코드가 훨씬 깔끔해진다.$ npm i redex-devtools-extensions
import { composeWithDevTools } from 'redux-devtools-extension'; const store = createStore(rootReducer,composeWithDevTools());
(이거 안해도.. redux탭에서 다 보이지 않ㄴ나.... ? 꼭 필요한 작업인지 .. )
컨테이너 컴포넌트 만들기
이제 리덕스 스토어에 접근하여 원한는 상태를 받아오고 액션도 디스패치 해야한다.
리덕스 스토어와 연동된 컴포넌트를 컨테이너 컴포넌트라고 부른다. 컨테이너 컴포넌트를 만들어보자
CounterContainer
src/containers/CounterContainer.jsx
import React from 'react' 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);
컴포넌트와 리덕스를 연동하려면 react-redux에서 제공하는 connect함수를 사용해야 한다.
connect(mapStateToProps, mapDispatchToProps)(연동컨테이너)
mapStateToProps
: 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기위해 설정하는 함수. 현재 스토어가 지니고있는상태
를 가리킴.mapDispatchToProps
: 액션 생성함수를 컴포넌트의 props로 넘겨주기위해 사용하는 함수. store의dispatch
를 파라미터로 받아온다.
connect를 호출하고나면 또 다른 함수를 반환하는데, 거기에 컴포넌트(컨테이너)를 파라미터로 넣어주면 리덕스와 연동된 컴포넌트가 만들어진다.
*mapStateToProps
, mapDispatchToProps
에서 반환하는 객체 의 내부 값은 CounterContainer
의 props
로 전달된다. *
App에서 CounterContainer 컴포넌트로 교체
App에서 Counter를 CounterContainer 컴포넌트로 교체한다.
import React from 'react' import Counter from '../components/Counter' import { connect } from 'react-redux' 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: () => { // console.log('increase'); dispatch(increase()) }, decrease: () => { // console.log('decrease'); dispatch(decrease()) } }) export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
그리고 액션생성함수를 import해서 액션객체를 만들고 dispatch했다.
리덕스에서 액션과 state가 잘 변하고있다.
mapStateToProps, mapDispatchToProps를 익명함수로 바로 작성
export default connect( state => ({ number: state.counter.number }), dispatch => ({ increase: () => dispatch(increase()), decrease: () => dispatch(decrease()) }))(CounterContainer);
mapStateToProps, mapDispatchToProps를 익명함수로 바로 작성하는 것도 가능하다.
하지만 컴포넌트에서 액션을 디스패치하기위해 각 액션생성함수를 호출하고 디스패치로 감싸는작업이 번거롭다. bindActionCreators
유틸함수를 사용하면 간편하다.
bindActionCreators 유틸함수
export default connect( state => ({ number: state.counter.number }), dispatch => bindActionCreators( { increase, decrease }, dispatch ) )(CounterContainer);
이렇게 작성하면되는데, 더 간편한 방법이 있다.
최종 간단 export 방법
// 파라미터에 함수형태가 아닌 액션 생성함수로 이루어진 객체형태 넣기 export default connect( (state) => ({ number: state.counter.number, }), { increase, decrease, } )(CounterContainer);
mapDispatchToProps에 해당하는 파라미터에 함수형태가 아닌 액션 생성함수로 이루어진 객체형태로 넣으면 더 간단해진다.
*connect함수가 내부적으로 bindActionCreators작업을 대신해준다. *
TodosContainer 만들기
import React from 'react' import Todos from '../components/Todos' import { connect } from "react-redux" import { changeInput,insert,toggle,remove } from '../modules/todos' const TodosContainer = ({ input, todos, changeInput, insert, toggle, remove }) => { return ( <Todos input={input} todos={todos} onChangeInput={changeInput} onInsert={insert} onToggle={toggle} onRemove={remove} /> ) } export default connect( // 비구조화 할당을 통해 todos 분리하여 // state.todos.input 대신 todos.input을 사용 ({ todos }) => ({ input: todos.input, todos: todos.todos }), { changeInput, insert, toggle, remove } )(TodosContainer)
connect를 호출하고나면 또 다른 함수를 반환하는데, 거기에 TodosContainer를 파라미터로 넣어주면 리덕스와 연동된 컴포넌트가 만들어졌다.
'Frontend > React' 카테고리의 다른 글
[React] 리액트 미들웨어 redux-thunk (1) | 2023.12.28 |
---|---|
[React] 리액트 미들웨어란? (0) | 2023.12.28 |
[React] Context API (0) | 2023.12.28 |
[React] immer를 사용해 쉽게 불변성 유지! (0) | 2023.12.28 |
[React] 컴포넌트 성능 최적화(React.memo와 useCallback, useReducer) (1) | 2023.12.28 |