티스토리 뷰
이전 포스트에서 redux의 용어와 개념에 대해서 설명했습니다. 이번 포스트에서는 실제 코드 예제를 살펴보고 어떻게 동작이 되는지 살펴 보겠습니다.
카운터 앱 예제
살펴볼 샘플 프로젝트는 버튼을 클릭 할 때 숫자를 더하거나 뺄 수 있는 작은 카운터 앱입니다. 간단하지만 react + redux 애플리케이션이 어떻게 동작하는지 알 수 있습니다.
이 샘플 프로젝트는 cra의 공식 redux템플릿(github.com/reduxjs/cra-template-redux)을 사용했습니다. redux-toolkit을 사용하여 redux store 및 로직을 만들고 react-redux(react-redux.js.org/)를 사용하여 redux store와 react 컴포넌트를 함께 연결 하는 표준 redux 구조로 구성되었습니다.
예제 코드 : codesandbox.io/s/github/reduxjs/redux-essentials-counter-example/tree/master/?from-embed
로컬에서 프로젝트를 만들고 싶다면 아래와 같이 만들수 있습니다.
npx create-react-app redux-essentials-example --template redux
카운터 앱 기능 및 DevTools
예제로 사용한 카운터 앱은 내부에서 일어나는 일을 볼 수 있도록 설정되어 있습니다.
브라우저에서 DevTools를 연 후 redux 탭을 선택하고 오른쪽 상단에서 State를 클릭합니다.
오른쪽 화면에서 redux store가 다음과 같은 상태 값으로 시작하는 것을 확인 할 수 있습니다.
{
counter: {
value: 0
}
}
DevTools는 앱을 사용 할 때 store안에 state가 어떻게 변경되는지 보여 줍니다.
앱을 실행 시킨 상태에서 "+"버튼을 클릭한 다음 redux devTools에서 Diff 탭을 확인합니다.
여기서 중요한 두 가지를 확인 할 수 있습니다.
- "+"버튼을 클릭했을 때 "counter/increment" type을 가진 action 객체가 store로 전달(dispatch)됩니다.
- 해당 action이 전달되었을 때 state.counter.value가 0에서 1로 변경되었습니다.
다음을 실행해 보세요
- "+"버튼을 다시 클릭하세요. 표시되는 값은 2가 됩니다
- "-"버튼을 클릭해보세요. 표시되는 값은 1이 됩니다
- "add mount"를 클릭해보세요 표시되는 값은 3이 됩니다
- input 박스의 숫자를 2에서 3으로 변경해보세요
- "add async"버튼을 클릭해보세요. 진행률 표시가 완료되면 값이 6으로 변경됩니다
react DevTools를 살펴보겠습니다. 버튼을 클릭할 때마다 action이 전달이 되서 5개의 action을 확인 할 수 있습니다.(좌)
"counter/incrementByAmount"의 왼쪽 목록에서 action탭을 선택합니다.(우)
action객체가 다음과 같은 것을 볼 수 있습니다
{
type: 'counter/incrementByAmount',
payload: 3
}
Diff 탭을 클릭하면 해당 action에 대한 응답으로 state.counter.value가 3에서 6으로 변경된 것을 확인 할 수 있습니다.
앱 내부에서 어떤 이벤트가 어떤 순서로 발생했는지 확인 할 수 있는 것은 디버그 할 때 매우 유용합니다.
이 외에도 DevTools에는 디버그 하는데 도움이 되는 몇 가지 추가 명령과 옵션이 있습니다. 오른쪽 상단의 'Trace'탭을 클릭해 보세요. action이 store에 도달 했을 때 어떤 코드들이 실행되었는지 스택으로 나타납니다.
이렇게 코드의 어떤 부분이 어떤 action을 전달했는지 쉽게 추적 할 수 있습니다.
애플리케이션 작동 방식
이번에는 앱이 어떻게 작동하는지 살펴보겠습니다.
이 애플리케이션을 구성하는 주요 파일은 다음과 같습니다.
- /src
- index.js: 앱의 시작점
- App.js: 최상위 React 컴포넌트
- /app
- store.js: Redux 스토어 인스턴스 생성
- /features
- /counter
- Counter.js: 카운터 기능에 대한 UI를 보여주는 React 컴포넌트
- counterSlice.js: 카운터 기능에 대한 Redux 로직
- /counter
redux store 생성
app/store.js는 다음과 같습니다
//app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
reducer: {
counter: counterReducer
}
})
redux store는 redux toolkit의 configureStore를 사용하여 생성됩니다. configureStore에 reducer를 전달합니다.
애플리케이션은 다양한 기능이 있을 수 있으며 각 기능 별로 reducer가 있을 수 있습니다. 이때 모든 reducer를 configureStore에 넣어 호출 할 수 있습니다. reducer객체 안의 key이름은 최종 state 값의 키값을 정의합니다.
features/counter/counterSlice.js 파일에는 카운터 로직에 대한 reducer를 내보내는 코드가 있습니다. 여기에서 counterReducer를 가지고 와서 store를 만들 때 포함 할 수 있습니다.
{ counter: counterReducer } 객체를 전달하면 redux state에 state.counter가 만들어지고, action이 dispatch 될 때마다 state.counter가 어떻게 업데이트 되는지 counterReducer에서 결정합니다.
redux를 사용할 때 다양한 플러그인(미들웨어 등)으로 store를 설정할 수 있습니다. configureStore는 여러 미들웨어가 설정되어 있고, 그 중 redux DevTools Extension를 통해 디버깅 할 수 있도록 설정되어 있습니다.
Redux Slice
slice는 일반적으로 앱에서 하나의 기능을 수행하는 action들과 redux reducer의 로직에 대해 모아놓은 하나의 파일입니다. root redux state를 여러 state로 분리하기 때문에 slice라고 부릅니다.
만약 블로깅 앱이 있다면 다음과 같이 할 수 있습니다
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'
export default configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
}
})
state.users, state.posts, state.comments는 redux state의 slice 입니다. usersReducer는 state.users slice의 업데이트를 담당합니다. 이를 slice reducer 함수라고 합니다
reducer와 state 구조
redux store는 생성 될 때 root reducer를 전달해야만 합니다. 그러면 서로 다른 slice reducer 함수가 여러 개 있는 경우 어떻게 하나의 root reducer를 전달하고, redux store state를 정의 할 수 있을까요?
모든 slice reducer를 호출하면 다음과 같습니다
function rootReducer(state = {}, action) {
return {
users: usersReducer(state.users, action),
posts: postsReducer(state.posts, action),
comments: commentsReducer(state.comments, action)
}
}
slice reducer(usersReducer, postsReducer, commentsReducer)에 맞는 redux state(state.users, state.posts, state.comments)를 전달합니다.
redux는 combineReducers함수를 사용하면 위와 같은 작업을 자동으로 해줍니다. slice reducer가 들어가있는 객체를 combineReducers에 인자로 넣으면 action이 전달될 때마다 각 slice reducer를 호출하는 함수를 반환합니다. 각 slice reducer의 결과는 최종적으로 하나의 객체로 결합 됩니다.
위의 코드를 conbineReducers를 사용하면 아래와 같습니다
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
})
combineReducers를 사용하여 생성한 root reducer를 configureStore에 직접 전달 할 수도 있습니다.
const store = configureStore({
reducer: rootReducer
})
Slice Reducers와 Actions 생성하기
features/counter/counterSlice.js 파일에는 있는 counteReducer와 해당 파일을 알아보겠습니다.
//features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// Redux Toolkit은 내무적으로 immer 라이브러리를 사용하고 있습니다.
// 따라서 아래와 같이 state.value를 직접 변경하여도
// immer로 인해 불변성을 지킬 수 있습니다
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
앞서 UI에서 다른 버튼을 클릭하면 세 가지 redux action type이 전달 되는 것을 확인 했습니다
- { type: "counter/increment" }
- { type: "counter/decrement" }
- { type: "counter/incrementByAmount" }
action은 type이라는 키,밸류가 있는 객체이고, type의 값은 항상 문자열이며, action 객체를 생성하는 action 생성자 함수가 있다는 것을 배웠습니다. 매번 손으로 작성을 해도 되지만 그것은 너무 비효율적입니다. 그렇다면 이런 action 객체, type 및 action 생성자는 어디에 정의 되어 있을까요?
redux toolkit에는 createSlice라는 action type, action 생성자 및 action 객체를 생성하는 작업을 하는 함수가 있습니다. slice의 이름을 정의하고, 그 안에 reducer 기능이 있는 객체를 작성하면 자동으로 생성됩니다. name 옵션은 각 action type의 첫번째 부분으로 사용되며 각 reducer의 기능을 담당하는 함수의 이름은 두번째 부분으로 사용됩니다.
예를들어 name을 "counter"로 설정하고, reducer의 기능 함수 이름을 "increment"설정하면 둘이 합쳐져서 { type: "counter/increment" } 이라는 action이 생성 됩니다.
name 필드 외에도 createSlice에 initialState를 전달하여 초기 state가 호출 될 수 있도록 해야합니다. 예제에서는 value가 0으로 시작하는 초기 state를 전달합니다.
예제에는 세가지의 reducer 함수가 있으며 각각의 버튼을 클릭하여 전달되는 세가지의 action type 확인 했습니다.
createSlice는 reducer 함수와 동일한 이름을 가진 action 생성자를 자동으로 생성합니다. 이를 확인 하고 싶으면 아래와 같이 확인 할 수 있습니다.
console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
또한 생성된 action type에 대해 응답할 수 있는 slice reducer도 생성됩니다
const newState = counterSlice.reducer(
{ value: 10 }, //state
counterSlice.actions.increment() // dispatch
)
console.log(newState)
// {value: 11}
Reducers의 규칙
앞에서 reducer는 항상 지켜야하는 규칙이 있다고 배웠습니다.
- 기존 state와 action을 기반으로 새로운 state를 계산해야합니다
- state를 직접 수정하면 안됩니다. 대신 기존 state를 복사하고 복사된 state를 변경하여 불변성을 지켜야합니다
- 비동기 로직이나 다른 side effect가 있으면 안됩니다
이런 규칙을 지켜야하는 이유는 다음과 같습니다
- redux의 목표 중 하나는 코드가 어떻게 동작하는지 예측 가능하게 하는 것 입니다. 함수의 return이 인수에 의해서만 계산되는 경우 해당 코드의 작동 방식을 이해하기 쉽고 테스트도 용이해 집니다.
- reducers가 외부 변수를 사용하거나 랜덤으로 동작하는 경우 함수가 실행 될 때 어떤 일이 발생하는지 정확히 알 수 없습니다.
- reducers가 인수(state)를 포함해서 다른 값을 수정하면 애플리케이션의 작동 방식이 변경될 수 있습니다. 이 경우 state를 업데이트를 했지만 UI가 업데이트 되지 않는 버그의 원인이 될 수 있습니다.
- redux DevTools 기능 중 일부는 reducer에 이런 규칙을 올바르게 적용해야 작동합니다
불변성 업데이트는 정말 중요하므로 더 자세하게 알아보겠습니다
Reducer 및 불변성(immutable) 업데이트
앞에서 자바스크립트의 배열/객체 spread 연산자와 원본 배열/객체의 복사본을 생성해서 반환하는 함수를 사용하여 불변성을 지키면서 업데이트하는 방법을 배웠습니다.
불변성을 지키며 업데이트하는 로직을 직접 작성하는 것은 손이 많이가고, redux를 쓸때 reducer에서 state를 변경하는 실수는 자주 일어납니다.
하지만 redux toolkit의 createSlice를 사용하여 쉽게 작성할 수 있습니다. createSlice는 immer 라이브러리가 내장되어 있습니다. immer는 불변성을 지키며 업데이트를 도와주는 라이브러리입니다.
예를 들면 아래와 같습니다
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
불변성을 지키기 위해 객체의 깊이가 깊어지면 위와 같이 복잡한 코드가 되지만 immer를 사용하면 아래와 같이 코드가 깔끔해집니다.
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}
여기에서 기억해야 할 중요한 사항이 있습니다.
redux toolkit의 createSlice와 createReducer는 immer가 내장되어 있기 때문에 불변성을 지키는 것을 신경쓰지 않고 객체를 변경하는 것처럼 코드를 작성하지만, immer가 없이 이렇게 작성한다면 버그를 유발합니다
counter slice에서 어떻게 작성을 하는지 알아보겠습니다
// features/counter/counterSlice.js
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// 이렇게 작성해도 immer가 불변성을 유지해줍니다
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
increment 함수는 state.value에 1을 더하는 코드입니다. immer는 초기 state 객체를 변경한다고 알고 있기 때문에 return을 할 필요가 없습니다. decrement도 동일합니다.
increment와 decrement는 action 객체를 받을 필요가 없습니다. dispatch는 되지만 꼭 필요하지는 않기 때문에 action을 매개변수로 받지 않아도 됩니다
반면에 incrementByAmount는 카운터 값에 얼마나 추가해야 하는지를 알아야합니다. 따라서 reducer에서 state와 action을 모두 인자로 받는 함수를 작성합니다. 이 경우 텍스트 input에 입력 한 값이 action.payload로 전달 되므로 state.value에 더할 수 있습니다.
불변성에 더 알고 싶다면 참고하세요 : daveceddia.com/react-redux-immutability-guide/
Thunks로 비동기 작성
지금까지의 애플리케이션 로직은 동기식으로 작동했습니다. action이 전달되고 store가 reducer를 실행하고 새로운 state를 계산해서 완료됩니다. 그러나 자바스크립트는 비동기 코드를 작성해야하는 여러 가지 방법이 있으며 일반적으로 API를 통해 데이터를 가져오는 작업이 있습니다. redux에서도 비동기 로직을 처리 할 때가 있습니다.
thunk는 비동기 로직을 처리하는 라이브러리 입니다. thunk는 두가지 함수를 사용하여 작성됩니다.
- dispatch와 getState를 인자로 받는 함수
- thunk를 생성하고 반환하는 외부 함수
다음 코드는 thunk action 생성자의 예시입니다
// features/counter/counterSlice.js
export const incrementAsync = amount => dispatch => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
일반적인 redux action생성자를 사용 할 수 있습니다
store.dispatch(incrementAsync(5))
그러나 thunk를 사용하려면 redux store가 생성될 때 redux-thunk 미들웨어를 추가해야합니다. redux toolkit에는 configureStore에는 이미 추가되어 있어서 thunk를 사용할 수 있습니다.
서버에서 데이터를 가져오기 위해 AJAX 호출을 해야하는 경우 이를 thunk에 넣을 수 있습니다. 다음 예제에서 확인해보세요
// features/counter/counterSlice.js
// the outside "thunk creator" function
const fetchUserById = userId => {
// the inside "thunk function"
return async (dispatch, getState) => {
try {
// make an async call in the thunk
const user = await userAPI.fetchById(userId)
// dispatch an action when we get the response back
dispatch(userLoaded(user))
} catch (err) {
// If something went wrong, handle it here
}
}
}
다음으로 counter 컴포넌트에 대해 살펴보겠습니다
Counter 컴포넌트
다음은 counter 컴포넌트의 코드입니다
// features/counter/Counter.js
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount
} from './counterSlice'
import styles from './Counter.module.css'
export function Counter() {
const count = useSelector(selectCount)
const dispatch = useDispatch()
const [incrementAmount, setIncrementAmount] = useState('2')
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
</div>
{/* omit additional rendering output here */}
</div>
)
}
counter 컴포넌트에는 useState를 사용해서 만든 데이터가 있습니다. 그러나 useState에는 count에 대한 값은 없습니다.
count는 useSelector를 통해 다른 곳에서 값을 가져오고 있습니다
react에는 useState와 useEffect같은 내장 hooks가 있지만, react의 hooks를 사용하여 custom hooks를 만들 수 있습니다. react-redux 라이브러리에는 react 컴포넌트가 redux store와 상호 작용할 수 있도록 만든 custom hooks이 있습니다.
useSelector를 사용해서 데이터 읽기
첫번째로 useSelector hook을 통해 컴포넌트가 redux store에서 필요한 데이터를 가져올 수 있습니다. 앞에서 state를 인수로 받아서 state 안에 일부분을 반환하는 selecor 함수를 만들 수 있는것을 배웠습니다.
counterSlice 파일의 하단에 아래와 같은 selector 함수가 있는것을 확인할 수 있습니다
// features/counter/counterSlice.js
// 이를 selecotr라고 하며 state에서 값을 선택할 수 있습니다
// selecor는 컴포넌트에서 useSelector안에 넣어도 됩니다
// ex) useSelector ((state) => state.counter.value)
export const selectCount = state => state.counter.value
redux sotre 접근할 수 있는 경우 아래와 같이 현재 counter의 값을 가져올 수 있습니다
const count = selectCount(store.getState())
console.log(count)
// 0
컴포넌트에서는 redux store를 직접 부를 수 없기 때문에 useSelector를 사용합니다. selector함수를 전달하면 someSelector(store.getState()) 를 호출하고 그에 대한 결과를 반환합니다
따라서 아래와 같은 코드로 store의 count값을 가져올 수 있습니다
const count = useSelector(selectCount)
export한 selector함수만 사용할 필요는 없습니다. 예를 들어 selector 함수를 useSelector에 인자로 넣어도 됩니다
const countPlusTwo = useSelector(state => state.counter.value + 2)
action이 전달되고 redux store가 업데이트 될 때마다 useSelector는 selector함수를 다시 실행합니다. selector가 이전 state와 다른 값을 반환하면 useselector는 새로운 state로 컴포넌트를 리렌더링합니다.
useDispatch를 사용해서 action 전달하기
redux store에 접근 할 수 있다면 store.dispatch(increment())와 같이 action 생성자를 사용하여 전달할 수 있습니다. store에 접근 할 수 없다면 store의 dispatch 메소드만 접근 할 수 있는 방법이 필요합니다.
useDispatch hook이 redux store의 dispatch 메소드를 제공합니다.
const dispatch = useDispatch()
어떤 버튼을 클릭할 때 다음과 같이 action을 전달 할 수 있습니다
// features/counter/Counter.js
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
컴포넌트 state와 Forms
앱 내의 모든 state를 redux store에 넣어야 할까요?
답은 아니오입니다. 앱 전체에 필요한 전역 state는 redux store에 있어야합니다. 한곳에서만 필요한 state는 컴포넌트에 있어도 됩니다.
사용자가 카운터에 추가 될 숫자를 입력하는 텍스트 input이 있습니다.
// features/counter/Counter.js
const [incrementAmount, setIncrementAmount] = useState('2')
// later
return (
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
onChange={e => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
onClick={() => dispatch(incrementByAmount(Number(incrementAmount) || 0))}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
>
Add Async
</button>
</div>
)
input의 onChange 핸들러에서 reducer에 action을 dispatch함으로써 state를 변경 할 수도 있지만 굳이 이렇게 할 필요는 없습니다. incrementAmount가 사용되는 컴포넌트는 counter 컴포넌트가 유일하기 때문입니다. 따라서 incrementAmount는 useState를 사용하는 것이 좋습니다.
마찬가지로 isDropdownOpen 같은 boolean state가 필요한 경우이면서 다른 컴포넌트에서 사용하지 않는다면 컴포넌트안에 있는 것이 좋습니다. React + Redux 앱에서 전역 state는 Redux store에 있고, 로컬 state는 React 컴포넌트에 있어야합니다.
state를 어디에 넣어야 할지 모르겠다면, redux에 어떤 데이터를 넣어야하는지 결정하는 몇가지 방법이 있습니다
- 애플리케이션에서 여러 컴포넌트가 데이터를 사용합니까?
- 원본 데이터를 기반으로 생성되는 다른 데이터가 있습니까?
- state가 어떻게 변경되는지 확인 할 필요가 있습니까?
- axios 등을 통해 가져온 데이터를 다시 요청을 보내지 않고 가져왔던 데이터를 그대로 사용하는 경우가 있습니까?
- UI 컴포넌트를 리로드 하는 동안 데이터를 유지해야 합니까?
이것은 일반적으로 redux에 forms를 어떻게 넣을지 생각하는 방법에 대한 좋은 예 입니다. 대부분의 forms에서 사용하는 state는 redux에 보관하면 안됩니다. 다만 컴포넌트에서 forms의 값을 사용자가 변경을 완료하면 redux에 전달하여 store를 업데이트합니다
다음으로 넘어가기 전에 주목해야 할 다른 사항: counterSlice.js의 incrementAsync thunk를 이 컴포넌트에서 사용하고 있습니다. 다른 일반 action 생성자를 전달하는 것과 같은 방식으로 사용합니다. 이 컴포넌트에서는 일반적인 action을 dispatch 하는지 아니면 비동기 로직을 시작하는지 신경쓰지 않습니다. 버튼을 클릭하면 무언가를 전달한다는 것만 알고 있습니다.
Providing the Store
컴포넌트가 useSelector 및 useDispatch를 사용하여 Redux store와 통신 할 수 있음을 확인했습니다. 그런데 store를 import 하지 않았습니다. 그렇다면 어떻게 어떤 Redux store와 통신해야하는지 알 수 있을까요?
애플리케이션의 다른 부분을 모두 살펴보았으니 이제는 시작점으로 가서 어떻게 이 퍼즐들이 맞춰지는지 살펴봅시다.
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
import * as serviceWorker from './serviceWorker'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
항상 ReactDOM.render(<App />)을 사용하여 react에게 root 컴포넌트인 App을 렌더링 하도록 했습니다. useSelector와 같은 hook이 제대로 작동하려면 <Provider> 컴포넌트를 사용하여 redux store를 연결시켜야합니다.
store를 app/store.js 에서 만들어 놓은 store를 import합니다. 그 후 <App />을 <Provider> 컴포넌트로 감싸고 store를 전달 합니다: <Provider store={store}>
모든 react 컴포넌트에서 이제 useSelector 또는 usedispatch를 통해 <Provider>에서 넘겨준 redux store와 연결 할 수 있습니다.
다음 장은 redux의 데이터 흐름에 대해 알아보겠습니다
'React' 카테고리의 다른 글
redux 강의 기본 핵심 튜토리얼 5탄 - redux data 사용하기 (0) | 2020.11.16 |
---|---|
redux 강의 기본 핵심 튜토리얼 4탄 - redux data flow (0) | 2020.11.11 |
redux 강의 기본 핵심 튜토리얼 2탄 - redux를 사용하기 위해 알아야하는 용어 및 개념 (0) | 2020.10.29 |
redux 강의 기본 핵심 튜토리얼 1탄 - redux란 무엇일까? (0) | 2020.10.29 |
react hooks에서 useContext(context API), useReducer로 상태관리 하기 (0) | 2020.10.29 |