상태 관리, 어떻게 하세요?라는 질문이 들어온다면...
전역 상태와 로컬 상태를 구분하여 전역 상태 관리가 필요할 때는 상태 관리 라이브러리를 사용하고, 그런 경우가 아니라면 useState와 props로 관리합니다.
상태 관리, 어떻게 하세요?
정적인 페이지를 만드는 것이 아닌 이상 상태 관리는 필수입니다.
하지만 무조건 상태 관리 라이브러리를 사용해야 하는것은 아닙니다. 마찬가지로 무조건 사용하지 않아야 하는 것 또한 아닙니다.
라이브러리를 사용하든, 사용하지 않든 상황과 목적에 맞게 사용해야 불필요한 코드를 작성하는 일을 피할 수 있습니다.
그러면 먼저 라이브러리를 사용하지 않아도 되는 상황은 무엇일까요?
간단한 예시로 구매하려는 물건의 수량을 변경하면 그에 따라 주문 금액이 변경되는 경우, 수량에 대한 상태와 주문 금액에 대한 상태가 하나의 컴포넌트안에 있다면 해당 컴포넌트에서만 수량과 주문 금액의 상태를 관리하면 되겠죠.
이렇게 특정 컴포넌트에서만 사용하는 상태를 관리할 때는 라이브러리를 사용하지 않아도 됩니다.
반대로 라이브러리를 사용해야 하는 상황은 무엇일까요?
마찬가지로 간단한 예시로 로그인 후 사용자의 id에 따라 특정 컴포넌트를 보여주어야 하는 경우, 사용자의 id를 저장하는 컴포넌트에서 특정 컴포넌트까지 전달하기에 깊이가 깊다면 이때는 사용자의 id를 담고 있는 상태를 전역 상태로 두어 특정 컴포넌트에서 직접적으로 해당 상태를 추출해서 사용하는 것이 좋습니다.
바로 이럴때 상태 관리 라이브러리를 사용하면 불필요한 전달없이 직접적으로 상태를 추출해 사용할 수 있는 것이죠.
Redux-toolkit 도입 계기
먼저 Redux는 상태 관리 라이브러리 중 가장 유명한 녀석이죠. 저는 상태 관리 라이브러리 중 맨 처음으로 접한게 Redux였고, React를 사용하면서 Redux를 불가결하게 생각했었습니다.(이름도 비슷하고 글자수도 같으니까요. ㅎㅎ 농담.)
그리고 프리 프로젝트에서 기술 스택을 리드 했을때 팀원마다 Redux에 대한 이해도와 숙련도가 달랐지만 팀원 모두가 알고 있고, 사용해본 경험이 있는게 Redux여서 2주라는 짧은 시간에 다른 상태 관리 라이브러리를 공부하고 사용하는 것보다 프로젝트에 바로 사용하는 것이 괜찮을 거라 생각했었습니다.
또한 한층 더 Redux와 전역 상태 관리에 대한 이해를 높이고, 메인 프로젝트에 가기전 좋은 훈련이되지 않을까란 생각이 들어 팀원분들과 회의 끝에 결정하게 되었습니다.
그 후 Redux 공식 문서에서 Redux-toolkit을 보면서 store 구성이 훨씬 간편하고, initial state와 reducers를 묶을 수 있는 등 여러가지 장점이 기존의 복잡한 Redux 보다 훨씬 간단하고 직관적일거라 느껴져 또 한번의 회의를 거쳐 최종으로 Redux-toolkit을 사용하기로 결정했습니다.
https://ko.redux.js.org/introduction/getting-started/
Redux 시작하기 | Redux
소개 > 시작하기: Redux를 배우고 사용하기 위한 자료
ko.redux.js.org
모든곳에서 전역 상태 관리가 필요한건 아니였다...
사실 프리 프로젝트에서는 상태 관리, 어떻게 하세요?에서 말했던 상태 관리에 대해 제대로 이해하지 못했었습니다.
당시에는 로컬 상태로만 관리했을 때의 불편함에 대해 꽂혀있었던지라 반드시 상태 관리 라이브러리가 필요한 것은 아니라는 생각을 안했던 것 같습니다...
아무튼 작년 말에 포트폴리오에 넣을 내용을 위해 다시 코드를 뜯어보던 때 AskEditBody 컴포넌트에서 사용한 React-Quill이 한글로 작성하면 자꾸 커서가 맨 앞으로 가는 에러가 났었고 그게 useEffect의 의존성 배열 문제 때문에 발생한 문제였다는 걸 떠올렸습니다.
// ...
useEffect(() => {
// 기존 질문(questionContent)을 askEditSlice의 content에 넣어주고
// questionContent를 의존성 배열에 넣어주면 커서 에러가 해결된다.
dispatch(setEditContent(questionContent));
}, [questionContent]);
// ...
다른건 문제 없어 보이니까 이 주제로 글을 써야지 생각하던 찰나에 조금 이상한 기분이 들었습니다.
'생각해보니까 useEffect를 굳이 여기서 사용할 필요가 있을까? 애초에 상위 컴포넌트에서 기존 질문을 넣어주고 props로 내려주면 되지 않았을까...?'란 의구심이 들었습니다.
그러고 상위 컴포넌트인 AskEdit 컴포넌트를 보고 깨달았습니다. 위에서 들었던 의구심 그대로 AskEdit에서 useEffect로 서버로 부터 기존 질문을 받아와 useState를 사용해서 기존 질문을 담은 state를 props로 내려주면 이 부분에선 전역적으로 상태관리를 하지 않아도 됐음을...
바보같이 왜 계속 전역 상태로 관리를 해왔냐라고 물으신다면...
단순히 전역 상태로 관리해야 나중에 컴포넌트 간 전달에서 일어날 불편함을 미리 피할 수 있다고 생각했고, AskEdit 부분을 만들기 전에도 전반적으로 상태를 전역 상태로 관리를 해왔기 때문이였습니다.
하지만 결국 이런 생각과 고집이 결국 간단하고 직관적으로 코드를 작성할 수 있었을텐데도 불구하고, 불필요한 전역 상태로 로직을 꼬이게 만들었습니다.
전역 상태를 로컬 상태로 변경
현재 깃허브엔 올리지 않았지만 AskEdit 컴포넌트에서 기존 질문을 받아와서 기존 질문의 제목과 글을 저장하고, 해당 상태를 AskEditTitle 컴포넌트와 AskEditBody 컴포넌트에 props로 내려주어 불필요하게 전역으로 상태를 관리하지 않아도 되는 훨씬 직관적이고 간단한 코드로 고쳤습니다.
AskEditTitle.jsx의 기존 코드
import React, { useEffect, useRef, useState } from 'react';
import { styled } from 'styled-components';
import AskEditTItle from '../components/askEdit/AskEditTItle.jsx';
import AskEditBody from '../components/askEdit/AskEditBody.jsx';
import AskEditTag from '../components/askEdit/AskEditTag.jsx';
import AskEditSummary from '../components/askEdit/AskEditSummary.jsx';
import AskRevision from '../components/askEdit/AskRevision.jsx';
import LoginNav from '../components/LoginNav.jsx';
import { useDispatch, useSelector } from 'react-redux';
import { getByQuestion } from '../redux/api/question/getByQuestion.js';
import { patchToAskEdit } from '../redux/api/askEdit/patchAskEditApi.js';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { resetAskEdit } from '../redux/feature/askEdit/askEditSlice.js';
import AskComment from '../components/askEdit/AskComment.jsx';
import AskComments from '../components/askEdit/AskComments.jsx';
const AskEdit = () => {
// 특정 질문에 대한 questionId를 받아오는 코드
const { questionId } = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
const [isFocus, setIsFocus] = useState(0);
const [comment, setComment] = useState(false);
// ...
const token = useSelector((state) => state.login.token);
const editData = useSelector((state) => state.askEdit);
//! 질문 상세 페이지가 구현이 완료되면 지우기
useEffect(() => {
dispatch(getByQuestion(questionId));
}, [questionId]);
const handleAskEditSumbit = async () => {
if (editData.title && editData.content) {
const action = await dispatch(
patchToAskEdit({
id: questionId,
title: editData.title,
content: editData.content,
token: token,
})
);
if (patchToAskEdit.fulfilled.match(action)) {
if (action.payload && action.payload.status === 200) {
navigate(`/questions/${questionId}`);
} else {
alert('제목과 내용을 다시 확인해 주세요.');
}
}
dispatch(resetAskEdit());
}
};
return (
<AskEditLayout>
{/* Nav ver.2 */}
<LoginNav />
<AskEditBox>
<AskEditItems>
<AskEditFormBox>
<AskEditFormItem>
<AskRevision />
<AskEditTItle isFocus={isFocus} setIsFocus={setIsFocus} />
<AskEditBody
modules={modules}
isFocus={isFocus}
setIsFocus={setIsFocus}
/>
<AskEditTag isFocus={isFocus} setIsFocus={setIsFocus} />
<AskEditSummary />
</AskEditFormItem>
<AskSubmitBox>
<AskSubmitButton onClick={handleAskEditSumbit}>
Save edits
</AskSubmitButton>
<CancelButton to={`/questions/${questionId}`}>
Cancel
</CancelButton>
</AskSubmitBox>
</AskEditFormBox>
<AddCommentBox>
{/*Todo: 생성한 Comment가 쌓이고 보일 수 있게 구현하기 */}
<AskComments />
<AddCommentText onClick={() => setComment(true)} comment={comment}>
Add a comment
</AddCommentText>
{comment && <AskComment setComment={setComment} />}
</AddCommentBox>
</AskEditItems>
</AskEditBox>
</AskEditLayout>
);
};
export default AskEdit;
AskEdit.jsx의 리팩토링한 코드
// AskEdit
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useParams } from 'react-router-dom';
import { getByQuestion } from '../redux/api/question/getByQuestion.js';
import {
AskEditTItle,
AskEditBody,
AskEditTag,
AskEditSummary,
AskRevision,
AskComment,
AskComments,
} from '../components/askEdit/AskEditTItle.jsx';
import LoginNav from '../components/LoginNav.jsx';
import { styled } from 'styled-components';
const AskEdit = () => {
// ...
const dispatch = useDispatch();
const { questionId } = useParams();
useEffect(() => {
dispatch(getByQuestion(questionId));
}, [questionId]);
const token = useSelector((state) => state.login.token);
const question = useSelector((state) => state.question);
const [isFocus, setIsFocus] = useState(0);
const [comment, setComment] = useState(false);
const [editAsk, setEditAsk] = useState({
title: question.title || '',
content: question.content || '',
});
const handleAskEditSumbit = () => {
if (editAsk.title && editAsk.content) {
const action = {
id: questionId,
title: editAsk.title,
content: editAsk.content,
token: token,
};
alert(`title:${action.title} content:${action.content}`);
}
};
return (
<AskEditLayout>
<LoginNav />
<AskEditBox>
<AskEditItems>
<AskEditFormBox>
<AskEditFormItem>
<AskRevision />
<AskEditTItle
isFocus={isFocus}
setIsFocus={setIsFocus}
editAsk={editAsk}
setEditAsk={setEditAsk}
/>
<AskEditBody
modules={modules}
isFocus={isFocus}
setIsFocus={setIsFocus}
editAsk={editAsk}
setEditAsk={setEditAsk}
/>
<AskEditTag isFocus={isFocus} setIsFocus={setIsFocus} />
<AskEditSummary />
</AskEditFormItem>
<AskSubmitBox>
<AskSubmitButton onClick={handleAskEditSumbit}>
Save edits
</AskSubmitButton>
<CancelButton to={`/questions/${questionId}`}>
Cancel
</CancelButton>
</AskSubmitBox>
</AskEditFormBox>
<AddCommentBox>
<AskComments />
<AddCommentText comment={comment} onClick={() => setComment(true)}>
Add a comment
</AddCommentText>
{comment && <AskComment setComment={setComment} />}
</AddCommentBox>
</AskEditItems>
</AskEditBox>
</AskEditLayout>
);
};
export default AskEdit;
끝으로
너무 정신없이 서비스 구현과 프로젝트 완성에만 신경을 썼다보니 너무 많은 스파게티 코드들이 있는 프로젝트라 포트폴리오에 올리기 까지 고민이 많이 되었습니다...
사실 지금 적은 상태 관리의 문제 뿐만 아니라 변수와 함수, 컴포넌트 명명부터 시작해서 배포와 도메인까지... 다시 차근차근 세워나가고 싶은 마음이 가득합니다. 하지만 아직은 현실적으로 불가능한 부분도 없잖아 있기 때문에(팀원분들... 정말 다시 리팩토링 할 생각없으신가효...? 혹시라도 글을 보신다면 연락주세요...) 그런 부분은 감안하고, 우선 중요한 일들을 끝내고 난 후 언젠가는 다시 도전해보고 싶네요. ㅎㅎ
그래도 프리 프로젝트에서 코드의 퀄리티...는 생각하지 못했지만, 처음으로 팀 단위로 프로젝트를 해보고, 배포까지 무탈없이 했기 때문에 메인 프로젝트로 가기 전 자신감을 넣어준 고마운 프로젝트였다고 생각합니다. 글을 읽어주신 모든 분께 감사합니다. :)