목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial
3편에서 RegisterPage.js 를 글 수정할 때도 재사용할 것이라고 언급한 적이 있었는데 RegisterPage 컴포넌트는 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트가 합쳐져 있는 상태라 재사용할 수 없는 구조여서 파일 구조 수정이 필요하게 되었다.
RegisterPage.js 가 들어있는 디렉토리만 바꾸면 되고 바뀐 구조는 이렇다
RegisterPage 를 컨테이너 컴포넌트로 사용하고 RegisterOrEdit을 프레젠테이셔널 컴포넌트로 사용할 예정이다.
containter component 와 presentational component에 대해선 이 redux 공식홈페이지에 잘 나와있다.
redux.js.org/basics/usage-with-react#presentational-and-container-components
우선 RegisterPage 컴포넌트에선 RegisterOrEdit 이라는 컴포넌트를 return 하는 방식으 대체했다.
(console.log 는 compile success 확인 여부이니 신경쓰지 않아도 된다)
RegisterOrEdit 컴포넌트의 모습은 DOM형태만 만들어주고 값과 이벤트는 RegisterPage의 props로 받아쓰므로 단순하게 생겼다. 사실 return 값은 이전 RegisterPage 컴포넌트와 거의 다를 게 없다.
console 창에 값이 잘 나오는지 확인해보자
이젠 form submit 부분을 만들 것이다.
우선 RegisterPage 컴포넌트에 이벤트를 간단하게 생성하고 RegisterOrEdit 컴포넌트에 props로 전달한다.
// RegisterPage.js
const onSubmitArticle = (event) => {
event.preventDefault();
const article = { title: TitleValue, content: ContentValue };
console.log(article);
};
return (
<>
<RegisterOrEdit
titleValue={TitleValue}
contentValue={ContentValue}
handleTitleChange={onTitleChange}
handleContentChange={onContentChange}
handleSubmit={onSubmitArticle}
/>
</>
);
값이 많을 때는 프레젠테이셔널 컴포넌트의 form 태그 내 value를 모두 긁어와서 setStateValue 하는 방법도 있지만 보낼 값이 적으니 객체 만들어 보내는 쉬운 방법으로 하기로 한다.
const article = { title: TitleValue, content: ContentValue };
RegisterOrEdit 컴포넌트에서 전달받은 handleSubmit 이벤트는 form 태그의 onSubmit 에 아직 적용시키지 않고 aritcle 값을 먼저 확인하기 위해 button의 onClick에만 적용한다.
// RegisterOrEdit.js
<button onClick={props.handleSubmit}>Submit</button>
파일을 저장하고 버튼을 클릭해보자
RegisterOrEdit 컴포넌트를 게시글 수정 때도 다시 사용하려면 새글 등록인지 글 수정인지 구별할 flag가 필요하다.
따라서 useState로 IsForUpdate 를 만들어주고 RegisterOrEdit 컴포넌트에 props로 넘긴다.
아직 인자 값이 없기 때문에 setIsForUpdate은 나중에 수정 기능을 만들 때 보기로 한다.
// RegisterPage.js
const [IsForUpdate, setIsForUpdate] = useState(false);
return (
<>
<RegisterOrEdit
titleValue={TitleValue}
contentValue={ContentValue}
handleTitleChange={onTitleChange}
handleContentChange={onContentChange}
handleSubmit={onSubmitArticle}
updateRequest={IsForUpdate}
/>
</>
);
RegisterOrEdit 컴포넌트에서는 RegisterPage 컴포넌트에서 보내준 IsForUpdate 값을 updateRequest로 받아 이 값이 true 이면 수정, false이면 등록으로 버튼이 바뀌어 나타날 수 있도록 코드를 수정해준다.
<button onClick={props.handleSubmit}>
{props.updateRequest ? "수정" : "등록"}
</button>
웹 화면에서는 이렇게 나타나야 한다.
수정이 완료된 RegisterPage 컴포넌트와 RegisterOrEdit 컴포넌트는 다음과 같은 모습이다.
이젠 저장할 값을 서버에 반영해야 할 차례다.
긴 여정이 될 것 같은데 필요한 모듈을 설치하고 reducer와 middleware인 saga를 만들어줘야 한다.
액션 -> 스토어 전달 -> 리듀서(reducer)에서 액션타입에 따라 다른 payload 전달 -> 상태 변경
이 흐름은 모두 동기적으로 일어나므로 API 같은 외부 리소스를 가져오는 경우 동기적인 리덕스 흐름만으로 해결할 수없다.
그래서 리듀서를 타기 전 혹은 액션에서 스토어 상태 변경 전에 비동기 로직을 끼워넣을 수 있는데 이를 미들웨어(middleware)라고 한다.
참고:
jeonghwan-kim.github.io/dev/2019/07/22/react-saga-ts-1.html
api 외부 리소스를 가져오는 데 사용하는 미들웨어로 대표적인 것은 redux-thunk와 redux-saga가 있으며 나는 redux-saga를 사용할 예정이다.
그리고 리듀서를 한 파일에서 쉽게 만들고 활용하기 위해서 redux-toolkit에서 제공하는 createAction, createSlice 등을 이용할 것이다.
(필요한 경우 다른 함수를 더 쓸 수도 있다.)
필요한 라이브러리를 설치하자.
root folder에서 terminal을 연다. (VSCode terminal을 kill 해도 되고 open native terminal 을 이용하여 열어도 된다)
terminal에 다음과 같이 입력한다.
yarn add redux-logger axios
설치가 끝나면 package.json에 반영되어 있을 것이다.
일단 새로 만들 파일과 그 파일들이 들어갈 디렉토리는 이렇게 생겼다.
sagas 디렉토리에는 saga 파일만 들어갈 것이고
slice 디렉토리에는 type, action, reducer 나 혹은 이 세 가지를 다 포함할 slice들이 만들어진 파일들이 들어갈 것이다.
saga와 slice가 종류별로 다양하지 않고 한 개만 있다면 rootSaga와 rootSlice는 굳이 안 만들어도 된다.
나는 혹시 몰라서 만들어놨다.
보통은 index.js로 만들어 놓는데 나중에 import할 때 헷갈릴까봐 이름을 달아놓았다.
reducer는 redux-toolkit의 createSlice로 만들어 사용할 건데 여기서부터는 redux를 이용한 상태 관리 흐름을 이미 알고 있다는 가정 하에 설명할 것이다.
모른다면 redux 공식홈페이지 설명을 먼저 읽고 간단한 tutorial을 한 뒤에 뒷 과정을 이어나가는 것을 추천한다.
#redux : actions
# redux : reducers
# redux: store
# redux : data flow
# redux : basic tutorial
그리고 이 사이트도 추천한다.
이곳도 마찬가지로 redux-saga와 redux-toolkit 를 동시에 사용했는데 특히 redux-toolkit 부분에서 엄청 도움이 되었다
mjn5027.tistory.com/39?category=1157737
우선 articleSlice.js 에 redux-toolkit 의 createSlice를 import 한다.
// /src/slice/articleSlice.js
import { createSlice } from "@reduxjs/toolkit";
redux-toolkit이 제공하는 API에서 createSlice 설명은 다음과 같다.
A function that accepts an initial state, an object full of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state.
출처: redux-toolkit.js.org/api/createSlice
그리고 추가할 내용은
// articleSlice.js
export const articleSlice = createSlice({
name: "article",
initialState: { id: 0, title: "", content: "", views: 0 },
reducers: {
registerArticle: (state, article) => {
console.log(article);
return {
...article,
id: state.id,
};
},
registerArticleAsync: (state, { payload }) => {
console.log(payload);
debugger;
return {
...state,
id: payload.id,
};
},
},
});
initialState는 인용문에서 말한 initial State,
reducers: {registerArticle: ..., registerArticleAsync: ,,} 는 reducer 함수가 있는 객체,
그리고 name은 slice name 이다.
createSlice는 자동으로 액션 타입과 액션 생성함수를 만들어준다.
액션 생성함수 없는 reducer를 extraReducer 로 추가할 수 있다고 API문서에 나와있지만 나는 일반 리듀서에서 해결해볼 것이다. (일단은)
그리고 이후에 saga에서 편히 쓰기 위해 articleSlice 변수에서 action과 reducer를 빼와 export 변수로 따로 만들어 아랫줄에 추가하였다.
// articleSlice.js
export const articleReducers = articleSlice.reducer;
export const articleActions = articleSlice.actions;
앞의 내용을 정리하면 articleSlice.js 파일의 형태는 다음과 같다.
// articleSlice.js
import { createSlice } from "@reduxjs/toolkit";
export const articleSlice = createSlice({
name: "article",
initialState: { id: 0, title: "", content: "", views: 0 },
reducers: {
registerArticle: (state, article) => {
console.log(article);
return {
...article,
id: state.id,
};
},
registerArticleAsync: (state, { payload }) => {
console.log(payload);
debugger;
return {
...state,
id: payload.id,
};
},
},
});
export const articleReducers = articleSlice.reducer;
export const articleActions = articleSlice.actions;
여기서 registerArtice 함수는 saga에서 감시할 액션으로 쓸 것이고
상태변경은 registerArticleAsync 함수는 서버 저장 후 게시물을 불러오는 데까지 이어 쓸 것이다.
articleSlice의 리듀서를 바로 store에 적용해도 되나 앞일은 모르니 rootSlice.js 파일을 만들어(앞에서 이미 만든 것과 동일한 파일임) 준다.
rootSlice는 사실 rootReducer라고 보면 되는데 /slice 디렉토리 안에 있기 때문에 이름을 rootSlice로 통일시킨 것뿐이다.
rootReducer라는 변수를 만들어 리듀서들을 묶어준다.
지금까지 사용한 리듀서는 일단 1개뿐이므로 articleReducer 하나만 import하여 combineReducer에 넣어준다.
그리고 /src 디렉토리에 바로 store.js 파일을 만들어준다.
redux-saga를 미들웨어로 쓸 것이기 때문에 createStore가 아닌 redux-toolkit의 configureStore를 사용한다.
// store.js
import { configureStore } from "@reduxjs/toolkit";
그리고 사용할 미들웨어를 import 한다.
// store.js
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
saga를 실행시켜줄 createSagaMiddleware을 sagaMiddleware라는 변수를 만들어 받아놓고
initialState 도 세팅해준다.
// store.js
const sagaMiddleware = createSagaMiddleware();
const initialState = {};
configureStore 함수 내 필요한 데이터를 집어놓고 store 라는 변수로 받아 둔다.
// store.js
const store = configureStore({
reducer: rootReducer,
devTools: true,
preloadedState: initialState,
});
export default store;
여기서 devTools 는 웹의 devTool 과 연결할 것인지 여부를 적는 것이다.
preloadState는 reducer에서 정해둔 initialState보다 더 앞서 prefix될 state이다.
redux-toolkit의 default middleware는 redux-thunk이기 때문에 sagaMiddleware 로 덮어씌워줘야 한다.
그러니 이젠 saga를 만들 차례다!
글 초반에 만들어둔 articleSaga.js 파일에 generate 함수를 만든다.
saga 는 generate 함수를 바탕으로 만들어야 한다.
// articleSaga.js
export function* registerArticleAsync(action) {
console.log(action);
debugger;
yield console.log("finish");
}
(값 확인을 위해 기본형태로만 만들었다)
generate 함수를 모른다면 아래 링크들을 참고하길 바란다.
wonism.github.io/javascript-generator/
developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Iterators_and_Generators
developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/function*
exploringjs.com/es6/ch_generators.html
saga 도 기능별로 다양하게 만들 수 있으므로 rootSaga를 하나 두기로 한다.
rootSaga 로서 사용할 함수도 generate 함수여야 한다.
generate 함수는 function에 별(*)이 붙어있는 모양으로 작성해야 하고 함수내 꼭 yield를 써줘야 한다. (안 그러면 오류남)
// rootSaga.js
import { takeLatest } from "redux-saga/effects";
import { articleActions } from "../slice/articleSlice";
import { registerArticleAsync } from "./articleSaga";
const { registerArticle } = articleActions;
export default function* rootWatcher() {
yield takeLatest(registerArticle.type, registerArticleAsync);
}
rootSaga.js 에서는 일단 뷰에서 registerArticle 액션생성함수를 dispatch 하게 되면 articleSaga의 registerArticleAsync 함수를 호출하도록 하였다.
그리고 다시 store.js로 돌아와서 rootSaga 를 store에 탑재해준다.
// store.js
import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import rootReducer from "./slice/rootSlice";
import rootSaga from "./sagas/rootSaga";
const sagaMiddleware = createSagaMiddleware();
const initialState = {};
const store = configureStore({
reducer: rootReducer,
middleware: [sagaMiddleware, logger],
devTools: true,
preloadedState: initialState,
});
sagaMiddleware.run(rootSaga);
export default store;
여기서 logger도 다른 종류의 미들웨어인데 prevState (previous state), action, next State 를 console 창에 보여주는 기능을 한다.
saga가 제대로 돌아가려면 꼭 run()을 해줘야 한다.
// store.js
sagaMiddleware.run(rootSaga);
store가 준비되었으니 index.js로 가서 store를 import 하자!
마지막으로 saga를 타도록 할 액션함수를 RegisterPage에서 dispatch 해야 한다.
RegisterPage 컴포넌트에서 useDispatch 라는 redux 훅과 articleSlice에서 빼둔 액션함수를 import하고 onSubmitArticle 함수를 다음과 같이 수정해야 한다.
// RegisterPage.js
import { useDispatch } from "react-redux";
import { articleActions } from "../../../slice/articleSlice";
function RegisterPage() {
const dispatch = useDispatch();
...
const onSubmitArticle = (event) => {
event.preventDefault();
const article = { title: TitleValue, content: ContentValue };
dispatch(articleActions.registerArticle(article));
};
return (
...
);
}
RegisterPage 컴포넌트의 최종적 모습은 다음과 같다.
그리고 RegisterOrEdit 컴포넌트의 form tag 에 onSubmit 이벤트도 마무리 해주자.
<form onSubmit={props.handleSubmit}>
...
</form>
등록 버튼을 눌렀을 때 articleSaga.js 에 적은 console.log까지 잘 가는지 확인해보자.
// articleSaga.js
import { articleActions } from "../slice/articleSlice";
export function* registerArticleAsync(action) {
console.log(action);
debugger;
yield console.log("finish");
}
Axios post 는 다음 편에서 다루겠다.
목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial