2021. 12. 9. 23:36ㆍReactJS/Information
Redux는 React.js 외에도 사용할 수 있는 상태관리 Library지만, 본 글은 React를 이용해 작성될 예정이다.
Redux란?
상태 관리를 진행해주는 Javascript Library이다.
React의 관심사를 분리해줄 수 있다.
상태 관리를 해야하는 이유?
React.js에서 상태(state)는 Component 단위로 관리를 진행하고 있다.
때문에 상위 Component에서 하위 Component로 이 상태를 내려주기 위해서는 Props를 이용해서 내려줘야 한다.
근데 만약 아래와 같이 Component가 관리되고 있고, Main Component에서 상태를 관리하고 있으며, 이 상태를 MyInfo까지 내려주려면 총 몇번을 내려야하는가?
[ Root Component ] > [ Main Component ] > [ MyInfo Component ] > [ MyInfo Edit Component ]
Main Component에서 상태를 만들고, MyInfo Edit Component의 상위 Component인 MyInfo Component로 Props를 내려주고, MyInfo Edit Component로 내려줘야 Main Component --> MyInfo Edit Component까지 상태를 전달해줄 수 있다.
위처럼 간단하고 Component가 몇개 없을 경우에는 2번만 내리면 되지만, 더 복잡하다면?
정말 끊임없이 내려줘야한다.
이런 현상을 Props drilling 이라고 한다.
위 현상을 상태 관리 Library인 Redux를 이용한다면 MyInfo Edit Component에서 바로 상태를 가져올 수 있다.
어떤 패턴을 사용하는가?
먼저 Redux에 대해 더 자세히 알아보기 전, Redux가 사용중인 패턴은 Flux 패턴으로 이 패턴에 대해 알고나면 더 쉽게 이해할 수 있다.
Redux Keyword
- Action
상태에 변화가 필요할 때 사용된다.
Action이 발생했을 때 어떤 Reducer를 실행시킬지 알아야 하기 때문에 Type은 필수로 사용해야 한다. - Action Creator
Component에서 더 쉽게 Action을 사용하기 위해 사용된다. - Reducer
변화를 일으키는 함수.
현재 상태와 Action을 참조하여 새로운 상태로 반환한다. - Store
한 Application 당 하나의 Global Store
현재 앱 상태, Reducer를 갖고 있으며 내장 함수를 포함하고 있다. - Dispatch
Store의 내장함수 중 하나다.
Dispatch를 통해 Action을 발생시킬 수 있다. - Subscribe
Store의 내장함수 중 하나다.
Action이 Dispatch될 때 마다 전달해준 함수가 호출된다.
connect(); 혹은 useSelector Hook을 사용한다.
Redux 3가지 규칙
- 하나의 Application 안에는 하나의 Store를 가져야 한다.
→ 필수는 아니지만, 여러개 생성에 대해 권장하지는 않는다.
개발 도구를 활용 할 수 없기 때문에 Debug나 상태 관리에 어려움이 있을 수 있다. - State는 읽기 전용이다.
→ 기존의 상태를 수정하지 않고, 새로운 상태를 생성하여 업데이트 하는 방식으로 사용된다.
(교체의 개념이며 Deep Copy를 생각하면 편하다.)
이를 통해 불변성 유지가 가능하며, Debug시 유용하다.
(Javascript는 주소값에 따라 Console.log에 찍어놔도 값이 계속해서 변하기 때문에..) - Reducer는 Pure Function(순수 함수) 이어야 한다.
→ Previous State와 Action Object를 Paramater로 받는다. (여기서 Action Object는 흔히 말하는 Payload다.)
→ Previous State는 건드리지 않고 새로운 State를 만들어 Return한다. (2번 규칙과 동일한 내용)
→ 동일 Input에 동일 Output이 나와야 한다.(순수 함수 조건)
(※ 하지만 이 내용은 때때로 잘 지켜지지 않을 때도 있다.
예를 들면 new Date(), Random Number.. 등 때에 따라 랜덤한 값을 줄 때 동일 Input에 동일 Output은 보장될 수 없다.
이 때 Redux Middleware를 사용한다.)
규칙과 패턴, Keyword를 모두 알아보았으니 이제 실제로 사용해보자.
(Javascript로 순수하게 Redux Pattern을 사용하지 않고, Redux-toolkit을 이용해 더 편하게 Reducer, Store, Action을 구현하도록 하겠다.)
Redux-toolkit은 ducks pattern을 지원하기 위해서 React에서 지원중인 Library로 사용하기 위해선 NPM에서 설치를 진행해야한다.
(ducks pattern은 본래 Redux Pattern을 사용하기 위해서는 Reducer, Store, Action을 전부 따로 구현을 해줘야 하며 파일별로 관리를 진행해야하는데 이 3개를 하나의 모듈로 관리할 수 있게 해주는 Pattern이다.)
Redux-toolkit은 보다 많이 지원을 하고 있으나, 여기에서는 createSlice를 이용해 관리하는 방법을 작성하도록 하겠다.
CreateSlice 살펴보기
1
2
3
4
5
6
7
8
|
function createSlice({
reducers : Object<string, ReducerFunction | ReducerAndPrepareObject>,
initialState: any,
name: string,
extraReducers?:
| Object<string, ReducerFunction>
| ((builder: ActionReducerMapBuilder<State>) => void)
})
|
cs |
- reducers : 모듈의 Reducer를 정의하면 된다.
이 때 reducers에 사용한 Key 값은 자동으로 Action Function으로 만들어준다. - initialState : 모듈의 초기 상태값, 그리고 상태의 구조를 확정지으면 된다.
- name : 모듈의 이름을 정의한다.
- extraReducer : 주로 redux-saga, redux-thunk API로 비동기 함수를 사용할 때 해당 부분에 정의한다.
정확한 정의는 아래와 같다.
"외부 작업을 참조하기 위한 것이기 때문에 slice.actions에 생성되지 않는다."
"ActionReducerMapBuilder를 수신하는 콜백으로 작성하는 것이 권장된다."
CRA(Create-React-App)를 이용해 간단하게 프로젝트 하나를 생성해주도록 하자.
create-react-app y0ngha-redux
.
.
.
.
Inside that directory, you can run several commands:
yarn start
Starts the development server.
yarn build
Bundles the app into static files for production.
yarn test
Starts the test runner.
yarn eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!
We suggest that you begin by typing:
cd y0ngha-redux
yarn start
Happy hacking!
(CRA로 만들면 맨날 즐거운 해킹을 하래..)
Redux-toolkit 및 Redux를 추가하도록 하자.
# NPM
npm install react-redux
npm install @reduxjs/toolkit
# yarn
yarn add react-redux
yarn add @reduxjs/toolkit
Typescript를 사용할 사람은 아래와 같이 Types도 추가해주도록 하자.
# NPM
npm install @types/react
npm install typescript
# yarn
yarn add @types/react
yarn add typescript
그리고 React를 실행해주도록 하자.
# NPM
npm start
# yarn
yarn start
자, 이제 프로젝트 생성도 끝났으니 Redux를 사용하는 간단한 예제를 만들어 보도록 하자.
먼저 src/ 폴더 안에 'redux' 라는 폴더를 추가해주고, 'redux' 폴더 안에 userSlice.ts를 만들어주도록 하자.
(폴더 명의 경우 강제되는 것은 아니나 Slice 관리를 위해 폴더를 분리하는 것을 추천한다.)
userSlice.ts에서는 사용자에 대한 정보를 관리할 것이며, userSlice에는 Redux-toolkit의 createSlice를 이용해 Action, State, Reducer를 모두 구현해줄 것 이다.
아래와 같이 소스를 작성하도록 하자.
userSlice.ts
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
|
import {createSlice} from "@reduxjs/toolkit";
export type UserSlice = {
users: User[],
}
export type User = {
userId: string,
userName: string,
userAge: number,
userBirthDay: string,
}
const initialState: UserSlice = {
users: []
}
const userSlice = createSlice({
name: 'userSlice',
initialState: initialState,
reducers: {
}
})
export const {} = userSlice.actions;
export default userSlice.reducer;
|
[ 3-5 ] : UserSlice State Type. 앞으로 Redux를 이용해 갖고올 때 이 타입으로 갖고올 것 이다.
[ 7-12 ] : User 정보를 갖고있는 Type.
[ 14-16 ] : UserSlice의 초기 State 값
[ 18-24 ] : Slice를 생성하는 라인. createSlice에 대해서는 위에 작성해놨으니 위로 스크롤을 올려 확인하길 바란다.
[ 26 ] userSlice에 정의된 actions를 destructuring 하여 가져오는 라인이다. 이 actions 안에는 reducers 안에 선언한 함수를 가져올 수 있다.
[ 27 ] : State를 가져오는 라인이다. userSlice.reducer는 initialState에 선언되어 있는 겍체를 가져올 수 있다.
다음으로 Store를 구현해주도록 하자.
Redux-toolkit을 이용해 Store를 구현하게 될 경우 CombineReducers(리듀서 결합) 작업이 필요 없다.
redux/store.ts에 작성하였다.
store.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import {configureStore} from "@reduxjs/toolkit";
import userReducer from "./userSlice";
import {useDispatch} from "react-redux"
export const store = configureStore({
reducer: {
userReducer
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
cs |
[ 5-9 ] : store에 대한 정의로 configureStore를 이용해 저장소를 하나 만들어줬다.
이 라인에 Reducer를 정의해 미리 정의해놓은 Reducer를 추가해놓고 사용할 수 있으며, 해당 라인에 middleWare를 등록하거나 devTools 등을 사용할 수 있다.
[ 11 ] : Store의 State타입을 정의해놓은 것이다. 나중에 Component에서 mapStateToProps를 이용해 connect할텐데 이 때 사용될 Type이다.
[ 12 ] : Store Dispatch의 Type을 정의해놓은 것이다.
[ 13 ] : dispatch를 통해 Store의 Action을 호출할텐데 이 때 편리하게 사용할 수 있도록 Hook 형식으로 만들어놓은 것이다.
이제 기본적인 설정은 끝났고, Component를 만들어 내가 만든 Redux가 정상적으로 작동하는지 확인해보도록 하겠다.
확인을 위해서 Presentational & ContainerPattern을 이용해 Component를 생성해주도록 하겠다.
Container Component는 총 2개를 만들었다. - UserRegister.tsx, UserList.tsx
Presentational Component는 총 1개를 만들었다. - UserInfo.tsx(User의 정보를 표시)
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {Provider} from "react-redux";
import {store} from "./redux/store";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App/>
</Provider>
</React.StrictMode>,
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();
|
cs |
11번째 라인을 추가하여 App에 store을 Provide 처리한다.
이 라인이 있어야 App 안에서 Redux Store에 접근할 수 있다.
그리고 userSlice에 addUser라는 Action을 추가해줬다.
userSlice.ts
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
|
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
export type UserSlice = {
users: User[],
}
export type User = {
userId: string,
userName: string,
userAge: number,
userBirthDay: string,
}
const initialState: UserSlice = {
users: []
}
const userSlice = createSlice({
name: 'userSlice',
initialState: initialState,
reducers: {
addUser: (state, action: PayloadAction<User>) => {
state.users = [...state.users, action.payload]
return state
},
}
})
export const { addUser, } = userSlice.actions;
export default userSlice.reducer;
|
cs |
addUser Action은 Payload에 User Type을 받고있으며, state의 users를 업데이트 해주고 있는 부분이다.
또 한 29번째 라인에서 addUser를 userSlice.actions에서 destructuring하여 export 해줬다.
(UserRegister Component에서 사용하기 위함)
UserRegister.tsx
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
|
import React from "react"
import "./UserRegister.scss"
import {RootState, store, useAppDispatch} from "../../redux/store";
import {connect} from "react-redux";
import {UserSlice} from "../../redux/userSlice";
interface IUserRegisterProps {
user: UserSlice
}
function UserRegister({user}: IUserRegisterProps) {
return (
<>
<div className={"userRegisterWrap"}>
<input type={"text"} className={"userId"} placeholder={"유저 ID 입력"}/> <input type={"text"} className={"userName"} placeholder={"유저명 입력"}/>
<input type={"text"} className={"userAge"} placeholder={"유저 나이 입력"}/>
<input type={"text"} className={"userBirthDay"} placeholder={"유저 생일 입력"}/>
<button className={"register"}>등록</button>
</div>
</>
)
}
function mapStateToProps(state: RootState) {
return {
user: state.userReducer
}
}
export default connect(mapStateToProps)(UserRegister);
|
cs |
UserRegister Component를 구현한 부분인데 설명하지 않았던 내용이 나와있다.
차근차근 설명할테니, 이해하면 된다.
[ 7-9 ] : UserRegister Component에서 받을 Props를 Interface로 만들어준 것이다.
안에서 user: UserSlice를 받고있는데 이 내용은 24-28번째 라인과 연관이 있으니 24-28번째 라인에서 설명하도록 하겠다.
[ 11 ] : {user}: IUserRegisterProps 는 UserRegister Component Props에서 user를 destructuring 하여 갖고오겠다는 내용이다.
[ 24-28 ] mapStateToProps(state: RootState) 함수는 Reducer와 UserRegister Component의 Props를 연결하기 위해서 사용된 함수로, store에서 현재 상태를 받아온다. 여기서 Paramater인 state는 29번째 Line에서 받아오고 있다.
[ 29 ] connect(mapStateToProps) 는 UserRegister와 mapStateToProps를 서로 연결하여 Props를 넣어주고 있는 것 이다.
App.tsx
1
2
3
4
5
6
7
8
9
10
11
|
import React from "react"
import UserRegister from "./component/container/UserRegister";
export default function App() {
return (
<>
<UserRegister/>
</>
)
}
|
cs |
App 안에는 UserRegister를 불러오도록 하였다.
이제 실행하고 결과를 보면 아래와 같은 화면이 나오게 된다.
많이 썰렁하니.. 일단 등록함수를 만들어주도록 하고 UserList Component를 구현해보도록 하겠다.
(mapStateToProsp, connect 등 해당 코드는 잘못 작성되었다. 실제로 코드에 적지는 말고 설명만 참고하도록 하자.)
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
46
47
48
49
50
51
52
|
import {RootState, store, useAppDispatch} from "../../redux/store";
import {connect} from "react-redux";
import {addUser, User, UserSlice} from "../../redux/userSlice";
import {ChangeEvent, useState} from "react";
interface IUserRegisterProps {
user: UserSlice
}
function UserRegister({user}: IUserRegisterProps) {
const dispatch = useAppDispatch()
const initialUser: User = {
userName: "",
userAge: 0,
userId: "",
userBirthDay: ""
}
const [newUser, setNewUser] = useState<User>(initialUser)
const onChangeHandler = (event: ChangeEvent<HTMLInputElement>) => {
setNewUser({
...newUser,
[event.target.className]: event.target.value
})
}
const onSave = () => {
dispatch(addUser(newUser))
setNewUser(initialUser)
alert("추가 완료!")
}
return (
<>
<div className={"userRegisterWrap"}>
<input type={"text"} className={"userId"} placeholder={"유저 ID 입력"} value={newUser.userId} onChange={(e) => {onChangeHandler(e)}}/>
<input type={"text"} className={"userName"} placeholder={"유저명 입력"} value={newUser.userName} onChange={(e) => {onChangeHandler(e)}}/>
<input type={"text"} className={"userAge"} placeholder={"유저 나이 입력"} value={newUser.userAge} onChange={(e) => {onChangeHandler(e)}}/>
<input type={"text"} className={"userBirthDay"} placeholder={"유저 생일 입력"} value={newUser.userBirthDay} onChange={(e) => {onChangeHandler(e)}}/>
<button className={"register"} onClick={() => {onSave()}}>등록</button>
</div>
</>
)
}
function mapStateToProps(state: RootState) {
return {
user: state.userReducer
}
}
export default connect(mapStateToProps)(UserRegister);
|
cs |
원래대로라면 이런식으로 코드를 짜게 될 경우 불필요한 렌더링이 많이 일어나 React.memo, useCallback Hook을 통해 코드 최적화를 해주는 것이 좋은데, 여기서는 설명하지 않도록 하겠다.
(설명하다가는 내용이 더 길어지고... 다른 내용으로 또 빠지는..)
[ 11 ] : store.ts 에서 선언한 useAppDispatch() 를 끌고온다.
이 dispatch는 store의 action을 할 때 사용될 예정이다.
[ 13-17 ] : 초기화시 사용될 User Type을 선언해준 것이다.
[ 19 ] : useState Hook을 이용해 상태값을 저장할 수 있도록 해줬다.
[ 20-26 ] : input에서 onChange가 발생될 경우 event를 받아서 className에 해당하는 Field에 값을 업데이트 해주도록 한다.
[ 27-31 ] : 등록 버튼을 눌렀을 때 dispatch를 이용해 addUser Action을 호출해준다.
이로써 input Tag에 작성된 User가 store에 들어가게 되는 것이다.
실제로 값을 넣고 등록을 누르면 일단 "추가 완료" 라는 Alert가 나오는 것은 보이지만, 실제로 추가가 됐는지는 안보인다.
이것을 확인하기 위해 UserList Component를 작성해주도록 하겠다.
UserList.tsx
1
2
3
4
5
6
7
8
|
export default function UserList() {
return (
<div className={"userList"}>
</div>
)
}
|
cs |
간단하게 userList라는 div를 생성해주고, 안에다가는 이제 Presentational Component인 User Component를 생성해 넣어줄 것이다.
UserInfo Component를 작성해주자.
UserInfo.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import {User} from "../../redux/userSlice";
interface IUserInfoProps {
user: User
}
export default function UserInfo({ user }: IUserInfoProps) {
return (
<>
<div className={"userWrap"}>
<span>
{user.userId} / {user.userName} / {user.userAge}살
</span>
</div>
</>
)
}
|
cs |
user를 Props로 받아와 단순히 뿌려주는 역할을 하는 컴포넌트를 만들었다.
이제 Container Component인 UserList Component로 돌아가 Redux Store에서 users를 뽑아와서 데이터를 User에 뿌려주면 등록된 사용자가 보일 것 이라고 기대할 수 있다.
한번 작성해보자.
(작성하다보니 UserList Component에 Redux Store를 연결했어야 했는데 UserRegister Component에 들어가버렸다... UserRegister에서 Redux Store를 연결하는 부분을 UserList로 옮겨주도록 하자.)
UserList.tsx
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
|
import {RootState} from "../../redux/store";
import {connect} from "react-redux";
import {User, UserSlice} from "../../redux/userSlice";
import UserInfo from "../presentational/UserInfo";
interface IUserListProps {
user: UserSlice
}
function UserList({user}: IUserListProps) {
return (
<div className={"userList"}>
{
user.users.map((mUser: User, index: number) => {
return (
<UserInfo key={index} user={mUser}/>
)
})
}
</div>
)
}
function mapStateToProps(state: RootState) {
return {
user: state.userReducer
}
}
export default connect(mapStateToProps)(UserList)
|
cs |
Redux Store에서 현재 상태를 받아와 map으로 Rendering 해주는 Component이다.
이제 이 Component를 App에 추가하고 결과를 보도록 하자.
App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
|
import UserRegister from "./component/container/UserRegister";
import UserList from "./component/container/UserList";
export default function App() {
return (
<>
<UserRegister/>
<UserList/>
</>
)
}
|
cs |
실행 결과 :
아주 잘 작동된다.
'ReactJS > Information' 카테고리의 다른 글
[React.js] createPortal 사용하여 DOM을 원하는 요소 안으로 옮기 (0) | 2021.12.11 |
---|---|
[React.js] 최상위 API React.memo (0) | 2021.12.11 |
[React.js] Hook 정리 (0) | 2021.12.11 |
[React.js] Re rendering 조건 (0) | 2021.12.09 |
[React.js] Class Component vs Functional Component (0) | 2021.12.04 |