4. redux-thunk : "์ก์ ๊ฐ์ฒด๊ฐ ์๋ ํจ์๋ฅผ ๋์คํจ์น ํ ์ ์๋ค."
=> ํจ์๋ฅผ ๋์คํจ์น ํ ๋์๋, ํด๋น ํจ์์์ dispatch์ getState๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์์์ฃผ์ด์ผ ํ๋ค. ์ด ํจ์๋ฅผ ๋ง๋ค์ด์ฃผ๋ ํจ์๋ฅผ thunk๋ผ๊ณ ๋ถ๋ฅธ๋ค.
const getComments = () => (dispatch, getState) => {
// ์ด ์์์๋ ์ก์
์ dispatch ํ ์๋ ์๊ณ
// getState๋ฅผ ์ฌ์ฉํ์ฌ ํ์ฌ ์ํ๋ ์กฐํ ํ ์ ์์ต๋๋ค.
const id = getState().post.activeId;
// ์์ฒญ์ด ์์ํ์์ ์๋ฆฌ๋ ์ก์
dispatch({ type: 'GET_COMMENTS' });
// ๋๊ธ์ ์กฐํํ๋ ํ๋ก๋ฏธ์ค๋ฅผ ๋ฐํํ๋ getComments ๊ฐ ์๋ค๊ณ ๊ฐ์ ํด๋ด
์๋ค.
api
.getComments(id) // ์์ฒญ์ ํ๊ณ
.then(comments => dispatch({ type: 'GET_COMMENTS_SUCCESS', id, comments })) // ์ฑ๊ณต์
.catch(e => dispatch({ type: 'GET_COMMENTS_ERROR', error: e })); // ์คํจ์
};
(async/await)
const getComments = () => async (dispatch, getState) => {
const id = getState().post.activeId;
dispatch({ type: 'GET_COMMENTS' });
try {
const comments = await api.getComments(id);
dispatch({ type: 'GET_COMMENTS_SUCCESS', id, comments });
} catch (e) {
dispatch({ type: 'GET_COMMENTS_ERROR', error: e });
}
}
$ yarn add redux-thunk
import ReduxThunk from 'redux-thunk';
<index.js>
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
const store = createStore(
rootReducer,
// logger ๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, logger๊ฐ ๊ฐ์ฅ ๋ง์ง๋ง์ ์์ผํฉ๋๋ค.
composeWithDevTools(applyMiddleware(ReduxThunk, logger))
); // ์ฌ๋ฌ๊ฐ์ ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ฉ ํ ์ ์์ต๋๋ค.
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
<modules/counter.js>
// ์ก์
ํ์
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
// ์ก์
์์ฑ ํจ์
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
// thunk ํจ์
// getState๋ฅผ ์ฐ์ง ์๋๋ค๋ฉด ๊ตณ์ด ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์์ฌ ํ์ ์์ต๋๋ค.
export const increaseAsync = () => dispatch => {
setTimeout(() => dispatch(increase()), 1000);
};
export const decreaseAsync = () => dispatch => {
setTimeout(() => dispatch(decrease()), 1000);
};
// ์ด๊น๊ฐ (์ํ๊ฐ ๊ฐ์ฒด๊ฐ ์๋๋ผ ๊ทธ๋ฅ ์ซ์์ฌ๋ ์๊ด ์์ต๋๋ค.)
const initialState = 0;
export default function counter(state = initialState, action) {
switch (action.type) {
case INCREASE:
return state + 1;
case DECREASE:
return state - 1;
default:
return state;
}
}
<containers/CounterContainer.js>
import React from 'react';
import Counter from '../components/Counter';
import { useSelector, useDispatch } from 'react-redux';
import { increaseAsync, decreaseAsync } from '../modules/counter';
function CounterContainer() {
const number = useSelector(state => state.counter);
const dispatch = useDispatch();
const onIncrease = () => {
dispatch(increaseAsync());
};
const onDecrease = () => {
dispatch(decreaseAsync());
};
return (
<Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />
);
}
export default CounterContainer;
5. redux-thunk๋ก ํ๋ก๋ฏธ์ค ๋ค๋ฃจ๊ธฐ
6. API ์ฌ๋ก๋ฉ ๋ฌธ์ ํด๊ฒฐํ๊ธฐ
๊ฐ์ง API ํจ์ ๋ง๋ค๊ธฐ
<api/posts.js>
// n ๋ฐ๋ฆฌ์ธ์ปจ๋๋์ ๊ธฐ๋ค๋ฆฌ๋ ํ๋ก๋ฏธ์ค๋ฅผ ๋ง๋ค์ด์ฃผ๋ ํจ์
const sleep = n => new Promise(resolve => setTimeout(resolve, n));
// ๊ฐ์ง ํฌ์คํธ ๋ชฉ๋ก ๋ฐ์ดํฐ
const posts = [
{
id: 1,
title: '๋ฆฌ๋์ค ๋ฏธ๋ค์จ์ด๋ฅผ ๋ฐฐ์๋ด
์๋ค',
body: '๋ฆฌ๋์ค ๋ฏธ๋ค์จ์ด๋ฅผ ์ง์ ๋ง๋ค์ด๋ณด๋ฉด ์ดํดํ๊ธฐ ์ฝ์ฃ .'
},
{
id: 2,
title: 'redux-thunk๋ฅผ ์ฌ์ฉํด๋ด
์๋ค',
body: 'redux-thunk๋ฅผ ์ฌ์ฉํด์ ๋น๋๊ธฐ ์์
์ ์ฒ๋ฆฌํด๋ด
์๋ค!'
},
{
id: 3,
title: 'redux-saga๋ ์ฌ์ฉํด๋ด
์๋ค',
body:
'๋์ค์ redux-saga๋ฅผ ์ฌ์ฉํด์ ๋น๋๊ธฐ ์์
์ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ๋ ๋ฐฐ์๋ณผ ๊ฑฐ์์.'
}
];
// ํฌ์คํธ ๋ชฉ๋ก์ ๊ฐ์ ธ์ค๋ ๋น๋๊ธฐ ํจ์
export const getPosts = async () => {
await sleep(500); // 0.5์ด ์ฌ๊ณ
return posts; // posts ๋ฐฐ์ด
};
// ID๋ก ํฌ์คํธ๋ฅผ ์กฐํํ๋ ๋น๋๊ธฐ ํจ์
export const getPostById = async id => {
await sleep(500); // 0.5์ด ์ฌ๊ณ
return posts.find(post => post.id === id); // id ๋ก ์ฐพ์์ ๋ฐํ
};
posts ๋ฆฌ๋์ค ๋ชจ๋ ์ค๋นํ๊ธฐ
- ํ๋ก๋ฏธ์ค๊ฐ ์์, ์ฑ๊ณต, ์คํจํ์๋ ๋ค๋ฅธ ์ก์ ์ ๋์คํจ์นํด์ผํฉ๋๋ค.
- ๊ฐ ํ๋ก๋ฏธ์ค๋ง๋ค thunk ํจ์๋ฅผ ๋ง๋ค์ด์ฃผ์ด์ผ ํฉ๋๋ค.
- ๋ฆฌ๋์์์ ์ก์ ์ ๋ฐ๋ผ ๋ก๋ฉ์ค, ๊ฒฐ๊ณผ, ์๋ฌ ์ํ๋ฅผ ๋ณ๊ฒฝํด์ฃผ์ด์ผ ํฉ๋๋ค.
<modules/posts.js>
import * as postsAPI from '../api/posts'; // api/posts ์์ ํจ์ ๋ชจ๋ ๋ถ๋ฌ์ค๊ธฐ
import {
createPromiseThunk,
reducerUtils,
handleAsyncActions,
createPromiseThunkById,
handleAsyncActionsById
} from '../lib/asyncUtils';
/* ์ก์
ํ์
*/
// ํฌ์คํธ ์ฌ๋ฌ๊ฐ ์กฐํํ๊ธฐ
const GET_POSTS = 'GET_POSTS'; // ์์ฒญ ์์
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // ์์ฒญ ์ฑ๊ณต
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // ์์ฒญ ์คํจ
// ํฌ์คํธ ํ๋ ์กฐํํ๊ธฐ
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';
// ์์ฃผ ์ฝ๊ฒ thunk ํจ์๋ฅผ ๋ง๋ค ์ ์๊ฒ ๋์์ต๋๋ค.
export const getPosts = createPromiseThunk(GET_POSTS, postsAPI.getPosts);
export const getPost = createPromiseThunkById(GET_POST, postsAPI.getPostById);
// initialState ์ชฝ๋ ๋ฐ๋ณต๋๋ ์ฝ๋๋ฅผ initial() ํจ์๋ฅผ ์ฌ์ฉํด์ ๋ฆฌํฉํ ๋ง ํ์ต๋๋ค.
const initialState = {
posts: reducerUtils.initial(),
post: {}
};
export default function posts(state = initialState, action) {
switch (action.type) {
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
return handleAsyncActionsById(GET_POST, 'post')(state, action);
default:
return state;
}
}
๋ฆฌ๋์ค ๋ชจ๋ ๋ฆฌํฉํ ๋งํ๊ธฐ
<lib/asyncUtils.js> createPromiseThunk, createPromiseThunkById ๋ง๋ค๊ธฐ
// ํน์ id ๋ฅผ ์ฒ๋ฆฌํ๋ Thunk ์์ฑํจ์
const defaultIdSelector = param => param;
export const createPromiseThunkById = (
type,
promiseCreator,
// ํ๋ผ๋ฏธํฐ์์ id ๋ฅผ ์ด๋ป๊ฒ ์ ํ ํ ์ง ์ ์ํ๋ ํจ์์
๋๋ค.
// ๊ธฐ๋ณธ ๊ฐ์ผ๋ก๋ ํ๋ผ๋ฏธํฐ๋ฅผ ๊ทธ๋๋ก id๋ก ์ฌ์ฉํฉ๋๋ค.
// ํ์ง๋ง ๋ง์ฝ ํ๋ผ๋ฏธํฐ๊ฐ { id: 1, details: true } ์ด๋ฐ ํํ๋ผ๋ฉด
// idSelector ๋ฅผ param => param.id ์ด๋ฐ์์ผ๋ก ์ค์ ํ ์ ์๊ณ์ฃ .
idSelector = defaultIdSelector
) => {
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
return param => async dispatch => {
const id = idSelector(param);
dispatch({ type, meta: id });
try {
const payload = await promiseCreator(param);
dispatch({ type: SUCCESS, payload, meta: id });
} catch (e) {
dispatch({ type: ERROR, error: true, payload: e, meta: id });
}
};
};
// ๋ฆฌ๋์์์ ์ฌ์ฉ ํ ์ ์๋ ์ฌ๋ฌ ์ ํธ ํจ์๋ค์
๋๋ค.
export const reducerUtils = {
// ์ด๊ธฐ ์ํ. ์ด๊ธฐ data ๊ฐ์ ๊ธฐ๋ณธ์ ์ผ๋ก null ์ด์ง๋ง
// ๋ฐ๊ฟ ์๋ ์์ต๋๋ค.
initial: (initialData = null) => ({
loading: false,
data: initialData,
error: null
}),
// ๋ก๋ฉ์ค ์ํ. prevState์ ๊ฒฝ์ฐ์ ๊ธฐ๋ณธ๊ฐ์ null ์ด์ง๋ง
// ๋ฐ๋ก ๊ฐ์ ์ง์ ํ๋ฉด null ๋ก ๋ฐ๊พธ์ง ์๊ณ ๋ค๋ฅธ ๊ฐ์ ์ ์ง์ํฌ ์ ์์ต๋๋ค.
loading: (prevState = null) => ({
loading: true,
data: prevState,
error: null
}),
// ์ฑ๊ณต ์ํ
success: payload => ({
loading: false,
data: payload,
error: null
}),
// ์คํจ ์ํ
error: error => ({
loading: false,
data: null,
error: error
})
};
// id๋ณ๋ก ์ฒ๋ฆฌํ๋ ์ ํธํจ์
export const handleAsyncActionsById = (type, key, keepData = false) => {
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
return (state, action) => {
const id = action.meta;
switch (action.type) {
case type:
return {
...state,
[key]: {
...state[key],
[id]: reducerUtils.loading(
// state[key][id]๊ฐ ๋ง๋ค์ด์ ธ์์ง ์์ ์๋ ์์ผ๋๊น ์ ํจ์ฑ์ ๋จผ์ ๊ฒ์ฌ ํ data ์กฐํ
keepData ? state[key][id] && state[key][id].data : null
)
}
};
case SUCCESS:
return {
...state,
[key]: {
...state[key],
[id]: reducerUtils.success(action.payload)
}
};
case ERROR:
return {
...state,
[key]: {
...state[key],
[id]: reducerUtils.error(action.payload)
}
};
default:
return state;
}
};
};
<modules/posts.js>
import * as postsAPI from '../api/posts'; // api/posts ์์ ํจ์ ๋ชจ๋ ๋ถ๋ฌ์ค๊ธฐ
import {
createPromiseThunk,
reducerUtils,
handleAsyncActions
} from '../lib/asyncUtils';
/* ์ก์
ํ์
*/
// ํฌ์คํธ ์ฌ๋ฌ๊ฐ ์กฐํํ๊ธฐ
const GET_POSTS = 'GET_POSTS'; // ์์ฒญ ์์
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // ์์ฒญ ์ฑ๊ณต
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // ์์ฒญ ์คํจ
// ํฌ์คํธ ํ๋ ์กฐํํ๊ธฐ
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';
// ํฌ์คํธ ๋น์ฐ๊ธฐ
const CLEAR_POST = 'CLEAR_POST';
// ์์ฃผ ์ฝ๊ฒ thunk ํจ์๋ฅผ ๋ง๋ค ์ ์๊ฒ ๋์์ต๋๋ค.
export const getPosts = createPromiseThunk(GET_POSTS, postsAPI.getPosts);
export const getPost = createPromiseThunk(GET_POST, postsAPI.getPostById);
export const clearPost = () => ({ type: CLEAR_POST });
// 3๋ฒ์งธ ์ธ์๋ฅผ ์ฌ์ฉํ๋ฉด withExtraArgument ์์ ๋ฃ์ด์ค ๊ฐ๋ค์ ์ฌ์ฉ ํ ์ ์์ต๋๋ค.
export const goToHome = () => (dispatch, getState, { history }) => {
history.push('/');
};
// initialState ์ชฝ๋ ๋ฐ๋ณต๋๋ ์ฝ๋๋ฅผ initial() ํจ์๋ฅผ ์ฌ์ฉํด์ ๋ฆฌํฉํ ๋ง ํ์ต๋๋ค.
const initialState = {
posts: reducerUtils.initial(),
post: reducerUtils.initial()
};
export default function posts(state = initialState, action) {
switch (action.type) {
case GET_POSTS:
case GET_POSTS_SUCCESS:
case GET_POSTS_ERROR:
const postsReducer = handleAsyncActions(GET_POSTS, 'posts');
return postsReducer(state, action);
case GET_POST:
case GET_POST_SUCCESS:
case GET_POST_ERROR:
const postReducer = handleAsyncActions(GET_POSTS, 'posts');
return postReducer(state, action);
case CLEAR_POST:
return {
...state,
post: reducerUtils.initial()
};
default:
return state;
}
}
<modules/index.js>
import { combineReducers } from 'redux';
import counter from './counter';
import posts from './posts';
const rootReducer = combineReducers({ counter, posts });
export default rootReducer;
ํฌ์คํธ ๋ชฉ๋ก ๊ตฌํํ๊ธฐ
<components/PostList.js>
import React from 'react';
function PostList({ posts }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>
{post.title}
</li>
))}
</ul>
);
}
export default PostList;
<containers/PostListContainer.js>
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PostList from '../components/PostList';
import { getPosts } from '../modules/posts';
function PostListContainer() {
const { data, loading, error } = useSelector(state => state.posts.posts);
const dispatch = useDispatch();
// ์ปดํฌ๋ํธ ๋ง์ดํธ ํ ํฌ์คํธ ๋ชฉ๋ก ์์ฒญ
useEffect(() => {
if (data) return; // API ๋ก๋ฉ ๋ฌธ์ ํด๊ฒฐ
dispatch(getPosts());
}, [data, dispatch]);
if (loading) return <div>๋ก๋ฉ์ค...</div>;
if (error) return <div>์๋ฌ ๋ฐ์!</div>;
if (!data) return null;
return <PostList posts={data} />;
}
export default PostListContainer;
<App.js>
import React from 'react';
import PostListContainer from './containers/PostListContainer';
function App() {
return <PostListContainer />;
}
export default App;
๋ฆฌ์กํธ ๋ผ์ฐํฐ ์ ์ฉํ๊ธฐ
$ yarn add react-router-dom
<index.js>
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import { BrowserRouter } from 'react-router-dom';
const store = createStore(
rootReducer,
// logger ๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, logger๊ฐ ๊ฐ์ฅ ๋ง์ง๋ง์ ์์ผํฉ๋๋ค.
composeWithDevTools(applyMiddleware(ReduxThunk, logger))
); // ์ฌ๋ฌ๊ฐ์ ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ฉ ํ ์ ์์ต๋๋ค.
ReactDOM.render(
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
ํฌ์คํธ ์กฐํํ๊ธฐ
<compontents/Post.js>
import React from 'react';
function Post({ post }) {
const { title, body } = post;
return (
<div>
<h1>{title}</h1>
<p>{body}</p>
</div>
);
}
export default Post;
<containers/PostContainer.js>
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getPost, goToHome } from '../modules/posts';
import Post from '../components/Post';
function PostContainer({ postId }) {
const { data, loading, error } = useSelector(
state => state.posts.post[postId]
) || {
loading: false,
data: null,
error: null
}; // ์์ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ์ง ์์ ๋๊ฐ ์์ผ๋ฏ๋ก, ๋น๊ตฌ์กฐํ ํ ๋น์ด ์ค๋ฅ๋์ง ์๋๋ก
const dispatch = useDispatch();
useEffect(() => {
if (data) return; // ํฌ์คํธ๊ฐ ์กด์ฌํ๋ฉด ์์ ์์ฒญ์ ํ์ง ์์
dispatch(getPost(postId));
}, [postId, dispatch, data]);
if (loading) return <div>๋ก๋ฉ์ค...</div>;
if (error) return <div>์๋ฌ ๋ฐ์!</div>;
if (!data) return null;
return (
<>
<button onClick={() => dispatch(goToHome())}>ํ์ผ๋ก ์ด๋</button>
<Post post={data} />
</>
);
}
export default PostContainer;
๋ผ์ฐํธ ์ค์ ํ๊ธฐ
<pages/PostListPage.js>
import React from 'react';
import PostListContainer from '../containers/PostListContainer';
function PostListPage() {
return <PostListContainer />;
}
export default PostListPage;
<pages/PostPage.js>
import React from 'react';
import PostContainer from '../containers/PostContainer';
function PostPage({ match }) {
const { id } = match.params; // URL ํ๋ผ๋ฏธํฐ ์กฐํํ๊ธฐ
// URL ํ๋ผ๋ฏธํฐ ๊ฐ์ ๋ฌธ์์ด์ด๊ธฐ ๋๋ฌธ์ parseInt ๋ฅผ ์ฌ์ฉํ์ฌ ์ซ์๋ก ๋ณํํด์ฃผ์ด์ผ ํฉ๋๋ค.
return <PostContainer postId={parseInt(id, 10)} />;
}
export default PostPage;
<App.js>
import React from 'react';
import { Route } from 'react-router-dom';
import PostListPage from './pages/PostListPage';
import PostPage from './pages/PostPage';
function App() {
return (
<>
<Route path="/" component={PostListPage} exact={true} />
<Route path="/:id" component={PostPage} />
</>
);
}
export default App;
<components/PostList.js>
import React from 'react';
import { Link } from 'react-router-dom';
function PostList({ posts }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
}
export default PostList;
7. thunk์์ ๋ผ์ฐํฐ ์ฐ๋ํ๊ธฐ
customHistory ๋ง๋ค์ด์ ์ ์ฉํ๊ธฐ
<index.js>
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
const customHistory = createBrowserHistory();
const store = createStore(
rootReducer,
// logger ๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, logger๊ฐ ๊ฐ์ฅ ๋ง์ง๋ง์ ์์ผํฉ๋๋ค.
composeWithDevTools(
applyMiddleware(
ReduxThunk.withExtraArgument({ history: customHistory }),
logger
)
)
); // ์ฌ๋ฌ๊ฐ์ ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ฉ ํ ์ ์์ต๋๋ค.
ReactDOM.render(
<Router history={customHistory}>
<Provider store={store}>
<App />
</Provider>
</Router>,
document.getElementById('root')
);
serviceWorker.unregister();
'โ๏ธ > React' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
React ๊ณต๋ถํ๊ธฐ(8) - ๋ฆฌ์กํธ ํ๋ก์ ํธ์์ ํ์ ์คํฌ๋ฆฝํธ ์ฌ์ฉํ๊ธฐ (0) | 2021.11.09 |
---|---|
React ๊ณต๋ถํ๊ธฐ(7) - redux-saga of ๋ฆฌ๋์ค ๋ฏธ๋ค์จ์ด (0) | 2021.11.09 |
React ๊ณต๋ถํ๊ธฐ(7) - ๋ฆฌ๋์ค ๋ฏธ๋ค์จ์ด (0) | 2021.11.09 |
React ๊ณต๋ถํ๊ธฐ(6) - ๋ฆฌ๋์ค(Redux) (0) | 2021.11.05 |
React ๊ณต๋ถํ๊ธฐ(5) - ๋ฆฌ์กํธ ๋ผ์ฐํฐ (0) | 2021.11.05 |