목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial
11편에서 a 태그와 router의 Link 컴포넌트의 차이점을 혹독히 경험하면서
응용으로 수정하기 버튼은 Link 컴포넌트로 만들 것이다.
그 전에 수정된 부분이 있는데 ArticleDetail에 있던 버튼을 ArticlePage 컴포넌트로 옮겼다.
변수를 좀 더 편하게 받기 위해서였다.
// ArticlePage
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { articleActions } from "../../../slice/articleSlice";
import ArticleDetail from "./Sections/ArticleDetail";
import { Button, Typography } from "antd";
import { Link } from "react-router-dom";
function ArticlePage({ match, location }) {
// console.log(match.params.articleId);
const dispatch = useDispatch();
useEffect(() => {
dispatch(articleActions.getArticle(match.params.articleId));
}, [match.params.articleId]);
const { id, title, content } = useSelector((state) => ({
id: state.articleReducers.id,
title: state.articleReducers.title,
content: state.articleReducers.content,
}));
const date = useSelector((state) => state.articleReducers.date);
const views = useSelector((state) => state.articleReducers.views);
return (
<div style={{ width: "80%", margin: "3rem auto" }}>
<div>
<ArticleDetail
id={id}
title={title}
content={content}
views={views}
date={date}
/>
</div>
<div style={{ margin: "2rem auto" }}>
<Link to={`/edit/${id}`}>
<Button type="primary">수정</Button>
</Link>
</div>
</div>
);
}
export default ArticlePage;
// ArticleDetail
import React from "react";
import { Button, Typography } from "antd";
const { Title } = Typography;
function ArticleDetail(props) {
return (
<div>
<div style={{ margin: "2rem auto" }}>
<a href="/">
<Button type="primary">목록으로 가기</Button>
</a>
</div>
<div style={{ textAlign: "center" }}>
<Title>게시글</Title>
</div>
<div>
<table>
<colgroup>
<col width="10%" />
<col width="40%" />
<col width="10%" />
<col width="40%" />
</colgroup>
<tr>
<th>번호</th>
<td>{props.id}</td>
<th>조회수</th>
<td>{props.views}</td>
</tr>
<tr>
<th>제목</th>
<td colspan="3">{props.title}</td>
</tr>
<tr>
<th>내용</th>
<td colspan="3">{props.content}</td>
</tr>
</table>
</div>
</div>
);
}
export default ArticleDetail;
react router API에서 Link 컴포넌트를 찾아보면 수정버튼을 누를 때 해당 글의 state를 Link 컴포넌트의 프로퍼티로 바로 집어넣어줄 수 있다.
예를 들어
일반적으로 이렇게 간단히 쓰는데
<Link to={`/edit/${id}?isForEdit=true`}>
<Button type="primary">수정</Button>
</Link>
만약 링크로 보내줄 state가 적고 한정되어 추가할 게 없는 경우
이렇게 바로 Link 컴포넌트를 통해 보내주면 편하다.
<Link
to={{
pathname: `/edit/${id}`,
search: "?isForEdit=true",
state: {
id: id,
title: title,
content: content,
views: views,
},
}}
>
<Button type="primary">수정</Button>
</Link>
Link 컴포넌트에서 state를 넘겨준 후 위 글에서 수정을 눌렀을 때
RegisterPage에서 console.log(props) 의 값을 보면
props.location.state 부분에 앞서 Link 컴포넌트에서 넘겨주었던 state 값이 들어가 있는 걸 알 수 있다.
Link 컴포넌트에서 필요한 state 값을 넘겨주는 방식으로 게시글 수정을 구현하면 RegisterPage에서 props로 state 값을 받기만 하고 내용을 조회하는 액션을 따로 dispatch하지 않아도 된다.
그러나 코드가 지저분해진다는 단점이 있고 새글등록 부분도 프로퍼티를 넘겨주는 방식으로 바꿔줘야 한다.
Link 컴포넌트에서 필요한 state를 넘겨줬을 때 어떤 문제점을 겪게 되는지 알아보겠다.
일단 ArticlePage 컴포넌트의 useSelector 를 이용하여 stateForProps를 추가한다.
const stateForProps = useSelector((state) => state.articleReducers)
ArticlePage를 보면 useSelector를 네 개나 썼는데 사실 useMemo 를 쓰면 이렇게 나눠 쓰지 않아도 된다.
근데 내가 useMemo를 잘 안 쓰기도 하고 어려워 해서 일단은 useSelector만으로 작성했다.
나중에 기회가 된다면 useMemo를 이용한 방법도 써서 포스팅해보겠다.
만들어진 stateForProps 를 Link 컴포넌트의 state 키의 값으로 넣는다.
// ArticlePage
<Link
to={{
pathname: `/edit/${id}`,
search: "?isEdit=true",
state: stateForProps,
}}
>
<Button type="primary">수정</Button>
</Link>
그러고 나서 수정버튼을 누른 후 state 값이 어떻게 나오는지 확인해보자
위의 값을 바탕으로 RegisterPage 컴포넌트에서 필요한 값들을 세팅해보자
// RegisterPage
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import RegisterOrEdit from "./Sections/RegisterOrEdit";
import { articleActions } from "../../../slice/articleSlice";
function RegisterPage(props) {
console.log(props);
const dispatch = useDispatch();
...
useEffect(() => {
const searchParams = new URLSearchParams(props.location.search);
if (searchParams.get("isForEdit")) {
setIsForUpdate(true);
}
setTitleValue(props.location.state.title);
setContentValue(props.location.state.content);
}, []);
...
const onSubmitArticle = (event) => {
event.preventDefault();
...
const article = {
title: TitleValue,
content: ContentValue,
views: views,
date: date,
editDate: editDate,
};
dispatch(articleActions.registerArticle(article));
};
...
}
export default RegisterPage;
useEffect 문을 보면 URLSearchParam을 이용하여 url에서 isForEdit이 true일 경우 setIsForUpdate true 값으로 바꿔주고 제목과 내용을 set 해주는 방식으로 했다.
URLSearchParam 을 처음 보는 사람들은 다음 사이트를 참고 바란다.
developer.mozilla.org/ko/docs/Web/API/URLSearchParams
여기서 단점은 새글 등록의 경우 props.location.state 값이 undefined 로 되어 있다.
물론 조건문 여러 개를 쓰거나 새글 등록 Link에서 같은 방법으로 state를 보내도 되지만
useSelector 를 이용할 경우 새글 등록 때는 초기값을 사용하고 글 수정 땐 Link를 통해 게시글 화면의 state 값을 가져와서 localStorage로 담거나 dispatch(조회 액션)을 쓰면 된다.
결국 같은 메소드로 두 방식을 한꺼번에 처리할 수 있는데 번거롭고 찾아보기도 힘들게 "굳이???" 이 방법을 써야 하나 싶은 것이다.
보기 편한 방법으로 코드를 다시 수정해보겠다.
ArtcilePage 컴포넌트에서 Link 컴포넌트를 다시 아래처럼 돌려놓는다. (stateForProps 만들어놓은 것은 삭제해버린다.)
// ArticlePage
<Link to={`/edit/${id}?isForEdit=true`}>
<Button type="primary">수정</Button>
</Link>
// ArticlePage
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { articleActions } from "../../../slice/articleSlice";
import ArticleDetail from "./Sections/ArticleDetail";
import { Button } from "antd";
import { Link } from "react-router-dom";
function ArticlePage({ match, location }) {
// console.log(match.params.articleId);
const dispatch = useDispatch();
useEffect(() => {
dispatch(articleActions.getArticle(match.params.articleId));
}, [match.params.articleId]);
const { id, title, content } = useSelector((state) => ({
id: state.articleReducers.id,
title: state.articleReducers.title,
content: state.articleReducers.content,
}));
const date = useSelector((state) => state.articleReducers.date);
const views = useSelector((state) => state.articleReducers.views);
return (
<div style={{ width: "80%", margin: "3rem auto" }}>
<div>
<ArticleDetail
id={id}
title={title}
content={content}
views={views}
date={date}
/>
</div>
<div style={{ margin: "2rem auto" }}>
<Link to={`/edit/${id}?isForEdit=true`}>
<Button type="primary">수정</Button>
</Link>
</div>
</div>
);
}
export default ArticlePage;
그리고 RegisterPage 컴포넌트의 useSelector 에 id, title, content 를 추가하고 바뀐 값에 맞게 useEffect 문도 수정한다.
// RegisterPage
const { id, title, content, views, date, editDate } = useSelector(
(state) => ({
id: state.articleReducers.id,
title: state.articleReducers.title,
content: state.articleReducers.content,
views: state.articleReducers.views,
date: state.articleReducers.date,
editDate: state.articleReducers.editDate,
})
);
useEffect(() => {
const searchParams = new URLSearchParams(props.location.search);
if (searchParams.get("isForEdit")) {
setIsForUpdate(true);
}
setTitleValue(title);
setContentValue(content);
}, []);
위 코드를 저장 후 게시글 화면에서 수정버튼을 누르면 변수 id, title, content, views, date, editDate 에 바로 값이 들어가지긴 한다.
그러나 새로고침하면 값이 다 날아간다.
이게 단순히 useState에서 다 초기값으로 설정되었기 때문에 그런 게 아니라 useState로 설정되어 있지 않은 views, date, editDate도 모두 날아간다.
개발자 창을 보면 알 수 있는데 게시글 화면을 조회할 때 getArticleAsync 액션으로 state 값은 변경되었지만 이것을 RegisterPage 에 props로 전달하지 않았기 때문에 새로고침할 시 state 값이 다 날아가는 것이다.
해결하는 방법은
1. props 로 전달
2. local storage 에 저장
3. 서버에서 해당 내용 조회
앞서 props로 전달하는 것은 하지 않기로 했으므로 나는 dispatch를 통해 서버에서 내용을 조회해오겠다.
우선 articleSlice 의 reducer 부분에 호출할 액션을 추가한다.
// articleSlice
fetchArticle: (state, {payload: id}) => {
console.log("게시글 조회 액션 호출 -- fetchArticle"); // saga에서 감시용
},
그리고 articleSaga 에 액션을 인터셉트하여 호출할 함수를 만든다.
export function* fetchArticleAsync(action) {
console.log(action);
const id = action.payload;
const response = yield Axios.get(`http://localhost:4000/board/${id}`);
yield put(articleActions.getArticleAsync(response.data));
}
위 코드를 보면 알 수 있듯 getArticleAsync를 재활용한다.
getArticleAsync의 내부는 이렇게 생겼다
// articleSlice
getArticleAsync: (state, { payload: article }) => {
console.log("saga에서 put 액션 호출 -- getArticleAsync");
return {
...state,
id: article.id,
title: article.title,
content: article.content,
date: article.date,
editDate: article.editDate,
views: article.views,
};
},
그리고 rootSaga에 감시할 액션과 호출할 함수를 추가해준다.
// rootSaga
export default function* rootWatcher() {
yield takeLatest(registerArticle.type, registerArticleAsync);
yield takeEvery(getArticle.type, getArticleAsync);
yield takeEvery(getBoard.type, getBoardAsync);
yield takeEvery(fetchArticle.type, fetchArticleAsync); // 추가된 부분
}
이제 뷰에서 dispatch 할 차례다.
// RegisterPage
const { id, views, date, editDate } = useSelector((state) => ({
id: state.articleReducers.id,
views: state.articleReducers.views,
date: state.articleReducers.date,
editDate: state.articleReducers.editDate,
}));
const { title } = useSelector((state) => ({ // 수정할 부분
title: state.articleReducers.title,
}));
const { content } = useSelector((state) => ({ // 수정할 부분
content: state.articleReducers.content,
}));
const [TitleValue, setTitleValue] = useState(title);
const [ContentValue, setContentValue] = useState(content);
const [IsForUpdate, setIsForUpdate] = useState(false);
useEffect(() => {
const searchParams = new URLSearchParams(props.location.search);
if (searchParams.get("isForEdit")) {
dispatch(articleActions.fetchArticle(props.match.params.articleId)); // 추가된 부분
setIsForUpdate(true);
}
setTitleValue(title);
setContentValue(content);
}, [id]); // 수정된 부분
useState 에 초깃값으로 state에서 select 해온 변수들을 넣어주고
(그 때문에 순서 위치를 바꿈)
useEffect문에 액션을 호출 부분을 추가해준다.
useSelector를 많이 쓴 이유는 렌더링할 때마다 불필요한 객체가 생성되는 게 싫어서, useMemo도 쓰기 싫어서인데
생긴 게 안 예쁘므로 shallowEqual로 바꿔준다.
// RegisterPage
const { id, views, date, editDate, title, content } = useSelector(
(state) => ({
id: state.articleReducers.id,
views: state.articleReducers.views,
date: state.articleReducers.date,
editDate: state.articleReducers.editDate,
title: state.articleReducers.title,
content: state.articleReducers.content,
}),
shallowEqual
);
shallowEqual 이 생소한 사람은 아래 사이트를 참조하길 바란다.
react.vlpt.us/redux/08-optimize-useSelector.html
(사실 둘의 내용은 같다)
코드가 아래처럼 수정되면
// RegisterPage
import React, { useEffect, useState } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import RegisterOrEdit from "./Sections/RegisterOrEdit";
import { articleActions } from "../../../slice/articleSlice";
function RegisterPage(props) {
console.log(props);
const dispatch = useDispatch();
const { id, views, date, editDate, title, content } = useSelector(
(state) => ({
id: state.articleReducers.id,
views: state.articleReducers.views,
date: state.articleReducers.date,
editDate: state.articleReducers.editDate,
title: state.articleReducers.title,
content: state.articleReducers.content,
}),
shallowEqual
);
const [TitleValue, setTitleValue] = useState(title);
const [ContentValue, setContentValue] = useState(content);
const [IsForUpdate, setIsForUpdate] = useState(false);
useEffect(() => {
const searchParams = new URLSearchParams(props.location.search);
if (searchParams.get("isForEdit")) {
dispatch(articleActions.fetchArticle(props.match.params.articleId));
setIsForUpdate(true);
}
setTitleValue(title);
setContentValue(content);
}, [id]);
const onTitleChange = (event) => {
setTitleValue(event.currentTarget.value);
};
const onContentChange = (event) => {
setContentValue(event.currentTarget.value);
};
const onSubmitArticle = (event) => {
event.preventDefault();
if (TitleValue === "" || TitleValue === null || TitleValue === undefined) {
alert("제목을 작성하십시오.");
return false;
}
if (
ContentValue === "" ||
ContentValue === null ||
ContentValue === undefined
) {
alert("내용을 작성하십시오.");
return false;
}
const article = {
id: id, ///
title: TitleValue,
content: ContentValue,
views: views,
date: date,
editDate: editDate,
};
console.log(article);
dispatch(articleActions.registerArticle(article));
};
return (
<>
<RegisterOrEdit
titleValue={TitleValue}
contentValue={ContentValue}
handleTitleChange={onTitleChange}
handleContentChange={onContentChange}
handleSubmit={onSubmitArticle}
updateRequest={IsForUpdate}
/>
</>
);
}
export default RegisterPage;
게시글 화면에서 수정을 눌렀을 때
내용이 잘 들어가져있고
막 아무글이나 쓴 다음에 새로고침을 하면
처음에 조회한 대로 값도 잘 들어가져 있다.
그리고 언급을 안 했었는데 dispatch 후 setIsForUpdate(true)를 해놨기 때문에 등록 버튼이 수정버튼으로 바뀌어 보이는 것이다.
이제 IsForUpdate 값을 이용하여 등록/수정버튼을 마무리지어보자.
먼저 articleSlice 에서 initialState 를 수정해주고 updateArticle 이란 액션을 만든다.
// articleSlice
import { createSlice } from "@reduxjs/toolkit";
export const articleSlice = createSlice({
name: "article",
initialState: {
id: 0,
title: "",
content: "",
views: 0,
date: Date.now(), // 수정
editDate: "", // 수정
},
reducers: {
registerArticle: (state, { payload: article }) => {
console.log("게시글 등록 액션 호출 -- registerArticle"); // saga에서 감시용
},
getArticle: (state, { payload: id }) => {
console.log("게시글 조회 액션 호출 -- getArticle"); // saga에서 감시용
},
getArticleAsync: (state, { payload: article }) => {
console.log("saga에서 put 액션 호출 -- getArticleAsync");
return {
...state,
id: article.id,
title: article.title,
content: article.content,
date: article.date,
editDate: article.editDate,
views: article.views,
};
},
fetchArticle: (state, { payload: id }) => {
console.log("게시글 조회 액션 호출 -- fetchArticle"); // saga에서 감시용
},
updateArticle: (state, { payload: article }) => {
console.log("게시글 수정 액션 호출 -- updateArticle"); // saga에서 감시용
},
},
});
export const articleReducers = articleSlice.reducer;
export const articleActions = articleSlice.actions;
그리고 articleSaga로 가서 updateArticleAsync를 만들어준다.
방식은 registerArticleAsync 와 거의 같고 Axios.get에서 Axios.put(id) 으로 바꿔준 것밖에 없다.
// articleSaga
export function* updateArticleAsync(action) {
const article = action.payload;
const response = yield Axios.put(
`http://localhost:4000/board/${article.id}`,
article
);
alert("저장되었습니다.");
console.log(response.data.id);
// history.push(`/article/${response.data.id}`);
history.push(`/article/${response.data.id}`, response.data.id);
}
그리고 rootSaga에 가서 액션과 saga를 연결해준다.
// rootSaga
import { take, takeEvery, takeLatest } from "redux-saga/effects";
import { articleActions } from "../slice/articleSlice";
import { boardActions } from "../slice/boardSlice";
import {
registerArticleAsync,
getArticleAsync,
fetchArticleAsync,
updateArticleAsync, // 이 부분
} from "./articleSaga";
import { getBoardAsync } from "./boardSaga";
const {
registerArticle,
getArticle,
fetchArticle,
updateArticle, // 이 부분
} = articleActions;
const { getBoard } = boardActions;
export default function* rootWatcher() {
yield takeLatest(registerArticle.type, registerArticleAsync);
yield takeEvery(getArticle.type, getArticleAsync);
yield takeEvery(getBoard.type, getBoardAsync);
yield takeEvery(fetchArticle.type, fetchArticleAsync);
yield takeLatest(updateArticle.type, updateArticleAsync); // 이 부분
}
그리고 RegisterPage 의 onSubmitArticle에 조건문을 추가한다.
// RegisterPage
const onSubmitArticle = (event) => {
event.preventDefault();
if (TitleValue === "" || TitleValue === null || TitleValue === undefined) {
alert("제목을 작성하십시오.");
return false;
}
if (
ContentValue === "" ||
ContentValue === null ||
ContentValue === undefined
) {
alert("내용을 작성하십시오.");
return false;
}
const article = {
id: id, ///
title: TitleValue,
content: ContentValue,
views: views,
date: date,
editDate: IsForUpdate ? Date.now() : editDate, // 수정
};
// console.log(article);
if (IsForUpdate) {
dispatch(articleActions.updateArticle(article)); // 추가
} else {
dispatch(articleActions.registerArticle(article));
}
};
작성한 파일들을 다 저장하고
수정버튼을 눌러보자
+
빠트린 부분과 수정할 부분 추가
BoardPage 컴포넌트에 New Post 버튼 링크에 쿼리가 추가되었다.
// BoardPage
<Link to="/register?isForEdit=false">
<Button type="primary">New Post</Button>
</Link>
그리고 RegisterPage 에 오타가 있어 바로잡는다. (고치지 않으면 새글 등록이 안된다. 등록버튼도 안 보임!)
// RegisterPage
useEffect(() => {
const searchParams = new URLSearchParams(props.location.search);
if (searchParams.get("isForEdit") === "true") { // 수정 부분
dispatch(articleActions.fetchArticle(props.match.params.articleId));
setIsForUpdate(true);
}
setTitleValue(title);
setContentValue(content);
}, [id]);
목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial