게시물을 삭제하는 기능을 만들면서, 뷰를 그리는 로직과 비즈니스 로직(서버의 데이터 변경 로직)을 구분해서 작성하게 되었다.
그 중에서 뷰를 그리는 로직을 구현한 방법에 대해 고민한 내용을 기록하고자 한다.
게시판의 게시물들은 서버에 차곡차곡 들어가 있다.
그리고 게시판을 렌더링할때, 서버에 차곡차곡 담긴 게시물 전체 데이터를 받아와서(GET) ArticleAtom이라는 Recoil Atom에 담아서 뿌려주고 있다.
게시물을 삭제할 경우, 서버에 게시물을 삭제해주세요~ 라는 DELETE 요청을 보내게 되는데, 이게 응답까지 1초정도 걸리는 상황이었다.
사용자가 삭제버튼을 클릭할 경우 ArticleAtom을 변경하면서 동시에 서버에 게시물 삭제 DELETE 요청을 보내는 방식으로 코드를 작성하였다.
사용자가 삭제를 했음에도 1초동안 게시물이 남아있다가 1초 후에 뿅! 사라지는 것이 좋지 않은 유저경험이 될 것이라 생각했기 때문에, 뷰에 빠르게 반영될 수 있도록 이런 방식으로 코드를 작성하게 된 것이다.
그리고, React Query를 사용하고 있지 않기 때문에 단순히 DELETE 요청을 보내더라도 뷰에는 변화가 없었다. ArticleAtom이 변하지 않는다면, 뷰는 변하지 않는 상황이었기 때문에 GET요청을 다시 보내야지만 사용자가 삭제한, 그러니까 아까의 DELETE 요청이 반영된 따끈따끈한 데이터를 받아와서 ArticleAtom를 변경해줄 수 있었다.
위에 말한 것과 같이, 사용자가 삭제버튼을 누르고 DELETE요청과 GET요청이 동기적으로 일어나는 방식으로 구현할 경우, 하나의 게시물을 삭제할때마다 GET 요청을 한번씩 보내게 된다.
GET요청을 그렇게 자주 보내는 것도 좋지 않은 로직이라고 생각되기도 한데, GET요청에 응답까지 3~6초까지 걸린다는 점이 굉장히 큰 문제였다.
사용자가 삭제를 누르면 게시물 데이터를 받아오는 3~6초를 로딩페이지를 보여주거나 빈 화면을 보여줘야하는 상황을 만들지 않기 위해서 뷰와 데이터 삭제를 분리했다.
아래의 응답값을 ArticleAtom에 집어넣고, setArticle 함수로 변경해준다.
//게시물 GET요청 응답값
[
{
"id": 1,
"email": "********", //로그인 유저의 email
"content": "1", //게시물의 text
"nickname": "이상조", //유저의 nickname
"imgUrl": "******.jpg", //유저의 profileImg
"likes": 0, //게시물의 좋아요 수
"likeYn": false, //현재 로그인된 유저가 이 게시물을 좋아요 했는가?
"imgs": [ blob ]//이미지는 blob으로 전달
},
{
"id": 2,
"email": "********",
"content": "2",
"nickname": "이상조",
"imgUrl": "******.jpg",
"likes": 1,
"likeYn": true,
"imgs": [ blob ]
},
{
"id": 3,
"email": "********",
"content": "3",
"nickname": "이상조",
"imgUrl": "******.jpg",
"likes": 1,
"likeYn": true,
"imgs": [ blob ]
}
]
이 ArticleAtom을 map 메서드로 뿌려주고 있다.
서버에서 데이터를 가져오는 것부터 map으로 뿌려주는 것까지 코드는 아래와 같다.
//axios.js
export async function getArticle(email) {
try {
const response = await axios.get(`${baseURL}/board?email=${email}`);
return response.data;
} catch (error) {
throw new Error(error);
}
}
//Community.jsx
function Community() {
const [article, setArticle] = useRecoilState(articleAtom);
const isDeleteModalOpen = useRecoilValue(articleDeleteAtom);
const [isLoading, setIsLoading] = useState(false);
const communityRef = useRef();
const get = async () => {
//조건문은 로그인/비로그인을 구분하기 위함.
if (localStorage.getItem("userInfo")) {
setIsLoading(true); //로딩중 화면을 띄워주기 위한 IsLoading state변경
const result = await getArticle(
JSON.parse(localStorage.getItem("userInfo")).email
);// 로그인 유저라면 email을 담아서 GET요청 보냄
setArticle(result); //결과를 Atom에 담음.
setIsLoading(false);
} else {
//비로그인 유저의 경우
setIsLoading(true);
const result = await getArticle(); //email 없이 GET
setArticle(result); //결과를 Atom에 담음.
setIsLoading(false);
}
};
useLayoutEffect(() => {
get();
}, []);
return (
<>
<LoadingPortal>{isLoading ? <LoadingScreen /> : null}</LoadingPortal>
{isDeleteModalOpen ? <ArticleDeleteModal /> : null}
<CommunityWrapper ref={communityRef}>
<CommunityTitle>
내가 만든 냉파 레시피{"\n"}자랑해봐요 👀
</CommunityTitle>
//map으로 article을 돌면서 게시물을 뿌려줌
{article.map((item) => (
<CommunityArticle {...item} key={item.id} />
))}
<BtnContainer width={communityRef.current?.offsetWidth}>
<ArticleWriteBtn />
</BtnContainer>
</CommunityWrapper>
</>
);
}
export default Community;
이 article을 수정하기위한 setArticle을 어떻게 사용해야할까?
setState는 state를 조작하는게 아니라 대체되는 형태이므로, prev를 복사하여 변경한 후 return하는 방식으로 사용한다.
즉, 기존 게시물 배열에서 삭제할 부분만을 삭제한 배열로 대체하게 된다.
그럼 내가 삭제하려는 게시물을 어떻게 타겟하여 삭제해야할까?
예를 들어, 요런 게시물 배열에서 3번을 삭제하고싶다면?
[{id: 1}, {id: 2}, {id: 3}]
그 방법에 대해 고민해보았다.
다른 개발자가 내 코드를 읽었을때 쉽게 이해가 가능한 코드를 짜기 위해서는 (성능상에 문제가 없다면) "어떻게"를 강조하는 명령형 방식이 더 적합할 때가 있다고 생각한다. 이렇게 말하면 대부분 선언형이 더 가독성이 좋은데요?! 라고 하실 것 같다.
그런데, 개발을 공부한지 오래되지 않은 개발자라면, 대부분 JS 기초 문법을 열심히 공부했을거라 더욱 명령형이 익숙하게 느껴지지 않을까.
그런 이유로 명령형, 그 중에서도 비효율적이지만 쉽게 떠올릴 수 있는 방법으로 게시물 삭제하는 로직을 만들어보았다.
//ArticleDeleteModal.jsx
const onDeleteBtnClick = (e) => {
//뷰를 변경하기 위한 setArticle
setArticle(() => {
let targetArticle;
for (let i = 0; i < article.length; i++) {
if (article[i].id === deleteArticleId) {
targetArticle = article.indexOf(article[i]);
}
}
// 반복문을 돌면서 '삭제버튼이 눌린 게시물 id'와 동일한 id를 가진 게시물을 찾고,
// 배열 내에서 몇번째인지, index를 targetArticle에 할당한다.
const copiedArticle = [...article];
copiedArticle.splice(targetArticle, 1); //targetArticle에 해당하는 원소를 삭제한다.
return copiedArticle;
});
deleteArticle(
deleteArticleId,
JSON.parse(localStorage.getItem("userInfo")).email
);
setDeleteArticleId("");
handleModal();
};
1년간 인프런 MD로 일하면서, 수많은 자바스크립트 입문 강의를 들었는데 대부분 이렇게 index를 찾아내서 원소를 제거하는 방식을 알려줬다.
입문 단계이기에 반복문에 대한 사용도 숙달하고, 명령형/선언형에 대한 고민을 하기보다는 기초 문법에 익숙해지게 하라는 의미라고 생각한다.
근데 이렇게 짜면 코드 길이가 길어진다.
풀어풀어 길게 설명하는 코드가 좋은 코드일까, 아니면 짧고 함축적인 코드가 좋은 코드일까?
그리고 이렇게 코드를 작성할 경우, targetArticle이라는 변수를 만들게 되는데 사이드이펙트는 없을까?
고민 끝에 선언적 방식으로 코드를 수정했다.
//ArticleDeleteModal.jsx
const onDeleteBtnClick = (e) => {
setArticle((prev) => prev.filter((item) => item.id !== deleteArticleId));
deleteArticle(
deleteArticleId,
JSON.parse(localStorage.getItem("userInfo")).email
);
setDeleteArticleId("");
handleModal();
};
filter 메서드를 사용함으로써 코드가 획기적으로 줄었다.
그리고 이전 방식이 '지워야할 게시물을 찾아서 지우고, 덮어쓴다!'였다면 지금 방식은 '지워야할 게시물을 제외한 나머지 게시물로만 배열을 만들어서 덮어쓴다!'로, 같은 효과를 내지만 방식은 다르다.
그리고 막상 짜놓고 보니, 가독성이 좋지 않다는 생각은 들지 않았다.
filter 메서드의 사용법을 알고 있다면 무리없이 해석할 수 있을거라 생각하여 이 방식으로 수정했다.
어떻게에 집중하는 명령형 프로그래밍,
무엇을에 집중하는 선언형 프로그래밍.
map, filter, forEach, reduce...
요런 선언적 메서드들을 사용하면서 함수형 프로그래밍에도 관심이 생겼다.
유인동님의 함수형 프로그래밍 강의를 조금 듣다가 말았는데...
수강평에 참 재미있는 내용이 많았던 걸로 기억한다.
함수형 프로그래밍의 big fan들이 많은건 다 이유가 있겠구나~?
꼭 수강해봐야겠다.
공부를 하고, 코드를 짜면서 느끼는 것은,
다른건 모르겠고! 코드 짜는 내 입장에서는 선언형이 확실히 편하구나! 라는 것이다.