티스토리 뷰
댓글 업데이트
commentList.jsx
수정 전
const handlePatch = () => {
dispatch(patchComment({ comment, id: params.id }));
};
수정 후
const handlePatch = (comment) => {
dispatch(
patchComment({
id: comment.id,
detailId: comment.detailId,
comment: content,
})
);
};
content는 새롭게 수정한 comment 내용임.
객체 자체를 갈아끼우는 것이므로 그 형식에 맞게 id, detailId(게시글 id), comment 모두 적어야 함!
수정 전
<Button
color="#61bfad"
value="완료"
onClick={() => {
handlePatch();
}}
>
수정 후
{comments &&
comments.map((item) => (
(생략)
<Button
color="#61bfad"
value="완료"
onClick={() => {
handlePatch(item);
}}
>
map 으로 comments 배열의 요소 하나하나를 보기 때문에 handlePatch() 안에 parameter 인자로 item을 넣어야 함!
commentSlice.js - extraReducers
수정 전
[patchComment.fulfilled]: (state, action) => {
state.isLoading = false;
state.comments = state.comments.map((item) => {
if (item.id === action.payload.id) {
return action.payload.comment;
}
return item;
});
수정 후
[patchComment.fulfilled]: (state, action) => {
state.isLoading = false;
state.comments = state.comments.map((comment) => comment.id === action.payload.id ? action.payload : comment)
},
comment의 id 가 action payload의 id와 일치하면 action의 payload를 뿌려주고 아니면 comment 를 그대로 돌려줘라.
즉 아이디가 같으면 수정된 객체 전체를 리턴하고 아니라면 그대로임
매니저님의 조언
📌 thunk 함수 쪽은 비동기적으로 처리되고 reducer 부분은 동기적으로 처리된다. json server에서 업데이트가 완료되었는데 extraReducer 부분에서 업데이트를 제대로 처리하지 않아서 생겼던 오류였음!!!
📌 폴더 구조 및 코드 작성 관련
- src 안에 페이지 이름으로 폴더를 만들고 그 안에 component와 module, style 파일을 다같이 넣어놓으면 페이지마다 수정하기 쉬움
- initialState는 그 데이터의 구조를 파악하기 쉽도록 보통 reducer 앞에 적어줌
- module js파일도 thunk 함수 부분과 reducer 부분을 나눠서 다른 파일에 적으면 나중에 프로젝트가 커졌을 때 관리하기가 쉬움.
- isLoading, error 부분은 return 문 안에 삼항연산자로 넣어서 주로 처리함
- 주석쓸 때 /**/ 이렇게 쓰면 어떤 부분인지 보여줌
📌 custom hook 은 크게 어려운 게 아니라 함수를 나눠서 빼놓은 것
+ cumstom hook 예시
useGetDiaryList.js
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getDiary, deleteDiary } from "../../../redux/modules/diarySlice";
export default function useGetDiaryList() {
const dispatch = useDispatch();
const { isLoading, error, diary } = useSelector((state) => state.diary);
const onDelete = (itemId) => {
// console.log(itemId);
dispatch(deleteDiary(itemId)).then(() => {
window.location.replace("/");
});
};
useEffect(() => {
dispatch(getDiary());
}, []);
return {
isLoading,
error,
diary,
onDelete,
};
}
이렇게 기존 작성한 함수를 가져오고 return문에서 isLoading, error, diary, onDelete 모두 리턴할 수 있음.
+ 댓글 삭제
const deleteHandler = (id) => {
dispatch(deleteComment(id));
// console.log(id);
};
댓글 삭제도 마찬가지로 댓글의 id값이 있어야 하므로 버튼의 onClick 함수 인자로 id를 넣어줘야 하고 reducer에도 id를 넣어줘야함
리액트 심화주차 전체 코드
*각 댓글 리스트마다 readonly 상태를 true/false로 주어야 하나씩 수정을 할 수 있는데 return 문 안에서 readonly 로 나눠버려서 하나를 선택하면 전체가 수정 활성화가 됐다. 이전에 todolist 를 만들 때 toggle로 isDone 상태를 true/false를 줬던 것처럼 지금도 데이터 하나하나마다 수정할 수 있는 상태값을 주었어야 했다! 아예 처음에 설계를 잘못 했고 컴포넌트를 쪼개지 않아서 더 헷갈렸던 것 같다.
commentList.jsx
import React, { useEffect, useState, memo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useParams } from "react-router-dom";
import styled from "styled-components";
import Button from "../../common/Button";
import Input from "../../common/Input";
import {
getComment,
postComment,
deleteComment,
patchComment,
} from "../../../redux/modules/commentSlice";
import { getDiaryId } from "../../../redux/modules/diarySlice";
function CommentList() {
const params = useParams();
const detail = useSelector((state) => state.diary.detail); // 일기 상세 아이디
const textList = useSelector((state) => state.comments.comments); // diary, comments 모두 있음
const { isLoading, error, comments } = useSelector((state) => state.comments);
const [input, setInput] = useState("");
const dispatch = useDispatch();
/** 댓글 수정 관련 */
const text = textList.find((ele) => ele.id === params.id);
const beforeContent = useRef(text?.content);
const [content, setContent] = useState(text?.content); // comment 내용
const [readonly, setReadOnly] = useState(true); // true 일 때 읽기 상태 false 일 때 textarea
// 수정 취소 버튼
const handleEdit = () => {
setContent(beforeContent.current);
setReadOnly(true);
};
// 수정 완료 버튼
const handlePatch = (comment) => {
dispatch(
patchComment({
id: comment.id,
detailId: comment.detailId,
comment: content,
})
);
};
// 댓글 input
const onChangeHandler = (event) => {
event.preventDefault();
setInput(event.target.value);
};
// 댓글 추가
const addHandler = () => {
if (input === "") {
alert("댓글을 입력해주세요");
} else {
dispatch(postComment({ comment: input, detailId: params.id })); // key: value
setInput(""); // 값입력된 후 초기화
}
};
// 댓글 삭제
const deleteHandler = (id) => {
dispatch(deleteComment(id));
// console.log(id);
};
// 처음 mount 될 때와 getComment() dispatch 가 실행될 때 렌더링됨
// 댓글 가져오기
useEffect(() => {
dispatch(getComment({ detailId: params.id }));
}, [dispatch, params]);
// 일기 상세정보 가져오기
useEffect(() => {
dispatch(getDiaryId(params.id));
}, [dispatch, params.id]);
if (isLoading) {
return <div>로딩 중....</div>;
}
if (error) {
return <div>{error.message}</div>;
}
return (
<DiaryDetail>
<LinkDiv>
<Link to="/">
<Button color="#ff8b8b">돌아가기</Button>
</Link>
<Link to="/write">
<Button color="#61bfad">다시 쓰기</Button>
</Link>
</LinkDiv>
<DetailDiv>
<Diary>
<div className="title">{detail.title}</div>
<div className="content">{detail.content}</div>
</Diary>
<InputBox>
<Input
type="text"
name="comment"
label="댓글을 작성해주세요"
value={input}
onChange={onChangeHandler}
/>
<Button type="button" onClick={addHandler} color="#61bfad">
댓글 쓰기
</Button>
</InputBox>
<ContentBox>
{readonly ? ( // {상태값? (ture) : (false)}
<ContentBox>{!content ? text?.content : content}</ContentBox>
) : (
// <ContentBox>읽기상태</ContentBox>
<textarea
className="textBox"
rows="10"
maxLength="200"
value={!content ? text?.content : content}
onChange={(e) => setContent(e.target.value)}
/>
)}
{}
</ContentBox>
<UpdateBox>
<div>
{comments &&
comments.map((item) => (
<div key={item.id}>
{/* <div style={{ borderBottom: '1px dotted #c4c4c4' }}> */}
<div>{item.comment}</div>
<div className="btnBox">
<Button
color="#ff8b8b"
type="button"
onClick={() => deleteHandler(item.id)}
>
삭제
</Button>
{readonly ? (
// 읽기 상태
<Button
color="#61bfad"
value="수정"
onClick={() => {
setReadOnly(!readonly);
}}
>
수정
</Button>
) : (
// 수정 상태
<>
<Button
color="#61bfad"
value="완료"
onClick={() => {
setReadOnly(!readonly);
// beforeContent.current = content;
handlePatch(item);
}}
>
완료
</Button>
<Button
color="#ff8b8b"
value="취소"
onClick={handleEdit}
>
{/* <Button color="#ff8b8b" value="취소"> */}
취소
</Button>
</>
)}
{/* <Button color="#61bfad" value="수정" onClick={()}>
수정
</Button> */}
</div>
</div>
))}
</div>
</UpdateBox>
</DetailDiv>
</DiaryDetail>
);
}
export default memo(CommentList);
// 컴포넌트의 state가 변경된 경우 - 불필요한 리렌더링 방지
const DiaryDetail = styled.div`
width: 100%;
padding: 20px;
`;
const LinkDiv = styled.div`
display: flex;
justify-content: space-between;
`;
const DetailDiv = styled.div`
max-width: 800px;
min-width: 600px;
margin: 30px auto;
padding: 20px;
`;
const Diary = styled.div`
margin-bottom: 20px;
border-bottom: 2px solid #333;
.title {
font-size: 26px;
margin: 10px 0 10px 0;
}
.content {
font-size: 24px;
margin-bottom: 20px;
}
`;
const InputBox = styled.div`
display: flex;
justify-content: space-between;
flex-direction: column;
padding-top: 10px;
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 2px solid #333;
input {
height: 50px;
text-indent: 10px;
font-size: 18px;
/* white-space: pre-line; */
}
button {
width: 80px;
height: auto;
font-size: 14px;
padding: 6px 6px;
margin-top: 10px;
margin-left: auto;
}
`;
const UpdateBox = styled.div`
.textArea li {
list-style: none;
}
.btnBox {
width: 100%;
display: flex;
justify-content: flex-end;
}
button {
width: 80px;
height: auto;
font-size: 14px;
padding: 6px 6px;
margin-top: 10px;
margin-right: 10px;
}
`;
const ContentBox = styled.div`
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: justify;
justify-content: space-between;
flex-direction: column;
margin-top: 50px;
/* min-height: 550px; */
line-height: 1.5;
font-size: 18px;
.textBox {
width: 100%;
border: 1px solid rgb(238, 238, 238);
padding: 12px;
font-size: 14px;
line-height: 1.5;
font-size: 18px;
}
`;
commentSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
// thunk 함수
// 댓글 가져오기
export const getComment = createAsyncThunk('comments/getcomment', async (payload, thunkAPI) => {
try {
const response = await axios.get(`http://localhost:3001/comments?detailId=${payload.detailId}`); // data 상수는 get 요청을 하겠다! 라는 뜻 자체
return thunkAPI.fulfillWithValue(response.data);
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
});
// ? 뒤에 붙는 게 querystring임
// https://www.google.com/search?q=검색어 이런 방식 get요청을 q 이렇게 하는 것
// http 통신방식 & axios 공부(비동기 통신 함수)
// 댓글 추가하기
export const postComment = createAsyncThunk('comments/postcomment', async (payload, thunkAPI) => {
try {
const response = await axios.post(`http://localhost:3001/comments`, payload); // url뒤에 querystring 으로도 보낼 수 있고 request 로도 보낼 수 있음
return thunkAPI.fulfillWithValue(response.data);
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
});
// 댓글 삭제하기
export const deleteComment = createAsyncThunk('comments/deletecomment', async (id, thunkAPI) => {
try {
await axios.delete(`http://localhost:3001/comments/${id}`);
return id;
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
});
// 댓글 수정하기
export const patchComment = createAsyncThunk(
'comment/patchcomment',
async (payload, thunkAPI) => {
try {
console.log('payload', payload)
// // payload를 데이터를 넣어줄때까지 실행하지 하지않겠다. //비동기
const data = await axios.put(`http://localhost:3001/comments/${payload.id}`, payload);
console.log('DB.JSON', data);
return thunkAPI.fulfillWithValue(data.data);
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
},
);
// 초기 상태 값
const initialState = {
comments: [],
isLoading: false,
error: null,
};
// createSlice API
export const commentSlice = createSlice({
name: 'comments', // 저장소의 이름
initialState,
reducers: {},
extraReducers: {
// 댓글 가져오기
[getComment.pending]: (state) => {
state.isLoading = true; // 네트워크 요청이 시작되면 로딩상태를 true로 변경합니다.
},
[getComment.fulfilled]: (state, action) => {
state.isLoading = false; // 네트워크 요청이 끝났으니, false로 변경합니다.
state.comments = action.payload; // Store에 있는 comments에 서버에서 가져온 comments를 넣습니다.
},
[getComment.rejected]: (state, action) => {
state.isLoading = false; // 에러가 발생했지만, 네트워크 요청이 끝났으니, false로 변경합니다.
state.error = action.payload; // catch 된 error 객체를 state.error에 넣습니다.
},
// 댓글 추가하기
[postComment.pending]: (state) => {
state.isLoading = true;
},
[postComment.fulfilled]: (state, action) => {
state.isLoading = false;
state.comments.push(action.payload);
},
[postComment.rejected]: (state, action) => {
state.isLoading = false;
state.error = action.payload;
},
// 댓글 삭제하기
[deleteComment.pending]: (state) => {
state.isLoading = true;
},
[deleteComment.fulfilled]: (state, action) => {
state.isLoading = false;
state.comments = state.comments.filter((item) => item.id !== action.payload);
},
[deleteComment.rejected]: (state, action) => {
state.isLoading = false;
state.error = action.payload;
},
// 댓글 수정하기
[patchComment.pending]: (state) => {
state.isLoading = true;
},
[patchComment.fulfilled]: (state, action) => {
state.isLoading = false;
state.comments = state.comments.map((comment) => comment.id === action.payload.id ? action.payload : comment)
},
[patchComment.rejected]: (state, action) => {
state.isLoading = false;
state.error = action.payload;
},
},
});
// Action Creator 는 컴포넌트에서 사용하기 위해 export
// export const {} = commentSlice.actions;
// reducer 는 configStore 에 등록하기 위해 export
export default commentSlice.reducer;
댓글 가져올 때 response를 어떻게 써야할 지 몰라서 댓글이 여러 게시글에 동시에 달렸었다. 이렇게 써줘야 한다고 함
// 댓글 가져오기
export const getComment = createAsyncThunk('comments/getcomment', async (payload, thunkAPI) => {
try {
const response = await axios.get(`http://localhost:3001/comments?detailId=${payload.detailId}`); // data 상수는 get 요청을 하겠다! 라는 뜻 자체
return thunkAPI.fulfillWithValue(response.data);
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
});
? 뒤에 붙는 게 querystring임
https://www.google.com/search?q=검색어 이런 방식 get요청을 q 이렇게 하는 것
-> json server 만의 규칙임!!!
(json server에서는 비밀번호가 맞는지 확인할 때 get으로 확인할 수 밖에 없기 때문에 이렇게 쓸 수 밖에 없는 것)
🌟 추가 공부
http 통신방식 & axios 공부(비동기 통신 함수)
useCallback() 써서 최적화하는 코드
'Projects' 카테고리의 다른 글
[Namoldak] EsLint & Prettier 설정 (0) | 2023.01.01 |
---|---|
[Modu-house] 미니 프로젝트 트러블 슈팅 (1) | 2022.12.19 |
[Project] React_TodoList (2) | 2022.11.30 |
[Mini Project] 풀스택 미니 프로젝트 발표 리뷰 (2) | 2022.11.18 |
[Mini Project] OTT Planet_11.14~11.17 (0) | 2022.11.18 |