게시글
, 댓글
, pagination
api요청이가능하며 모달창을 통해 페이지를 이동하도록 작성되어있다.const LIMIT_TAKE = 20;
const PostDetailPage = () => {
const [params] = useSearchParams();
const [postDetail, setPostDetail] = useState([]);
const [commentList, setCommentList] = useState([]);
const [isOpenCommentList, setIsOpenCommentList] = useState(false);
... {fetching함수}
... {UseEffetc()}
... {ClickEvent()}
return (
<div>
...{component}
...{PagiNation}
</div>
);
};
export default PostDetailPage;
위와같은 두개의 페이지컴포넌트가 존재하고 각페이지를 위한 PagiNation컴포넌트도 두개 존재하는 상황이었다.
==> Page컴포넌트가 Logic 과 State를 모두 관리하는것이 문제처럼 보여진다.
간단하게 코드의 기능과 문제점을 살펴봤으니 해결방법을 생각해보자.
다음과 같이 Refactor
를위해 설계를 하고 진행해보자.
1.비슷한 컴포넌트 재사용하기
const PostPageNation = () => {
...{생략}
const fetchPostPageNation = useCallback(async () => {
const response = await axios.get("/api/{여기만바뀜}, {
...{생략}
export default PostPageNation;
PagiNation컴포넌트는 PagiNation.Commnet
,PagiNation.Post
두가지가 존재하는데 자세히보니 {여기만 바뀜}에 해당하는 부분 -> 즉 api요청주소만 바뀌고있다.
두가지 컴포넌트를 path props를 전달받아 api요청을 다르게하는 하나의 PagiNation컴포넌트로 만들었다.
const PostPageNation = ({path}) => {
...{생략}
const fetchPostPageNation = useCallback(async () => {
const response = await axios.get(`/api/${path}`, {
...{생략}
export default PostPageNation;
2.logic 과 state 별도의 파일에서 분리하기.
logic과 state를 포함한 코드를 별도의 파일로 분리하기위해서 customhook을 이용하기로 했다.
/ use-fetch-data.js /
export const useFetchData = () => {
const [params, setParams] = useSearchParams()
const [postData, setPostData] = useState()
const [postDetail, setPostDetail] = useState()
const getParamValues = ({ keyArr }) => {
const urlObj = {}
keyArr.forEach((key) => (urlObj[key] = params.get(key)))
return urlObj
const getParamValues = () => {
return {
take: params.get(KEY.TAKE),
page: params.get(KEY.PAGE),
limit: params.get(KEY.LIMIT),
}
}
const setParamValues = ({ keyValueArr }) => {
keyValueArr.forEach((keyVal) => params.set(keyVal[0], keyVal[1]))
const setParamValues = ({ page, limit, take }) => {
params.set(KEY.PAGE, page)
params.set(KEY.LIMIT, limit)
params.set(KEY.TAKE, take)
setParams(params)
}
const fetchDataByFormAndAdd = async ({ form = "Posts", address }) => {
const response = await axios.get(`/api/${address}`, {
params: {
take: params.get(KEY.TAKE),
page: params.get(KEY.PAGE),
limit: params.get(KEY.LIMIT),
},
})
setPostData(response.data[form])
}
const fetchPostDetail = async () => {
const response = await axios.get("/api/post")
}
return {
getParamValues,
setParamValues,
fetchDataByFormAndAdd,
fetchPostDetail,
postData,
postDetail,
}
위와같이 하나의 hook함수를 생성해서 기능들을 빼내는것에는 성공을 했는데 .....
큰문제를 발견했다.
/ PostDetailPage.jsx /
PostDetailPage = () => {
const {
setParamValues,
getParamValues,
fetchPostDataByUrlAndDataForm,
fetchDataByFormAndAdd,
fetchPostDetail,
postData: commentList,
postDetail,
} = useFetchData()
useEffect(() => {
checkUserAuth()
fetchPostDetail()
setParamValues({ page: 1, take: 10, limit: 10 })
}, [])
const paramValues = getParamValues({
keyArr: [KEY.PAGE, KEY.LIMIT, KEY.TAKE],
})
const { page } = getParamValues()
useEffect(() => {
if (!isOpenCommentList) return
fetchDataByFormAndAdd({
form: "Comments",
address: "comments",
})
}, [page])
const onClickMoreComments = async () => {
setIsOpenCommentList(true)
await fetchDataByFormAndAdd({
form: "Comments",
address: "comments",
})
}
3.👀관심사 분리
방향이 잘못된거같아 검색을 하던중 좋은 글을 발견했다.
관심사 분리에관한 글 을 읽어보고 다시 방향을 잡을 수 있엇다.
☝ 재밌는 내용이니 읽어보세요~~
내가 생각했던 관심사 분리란 view를 담당하는 컴포넌트에서 logic을 추출 하기만 하면 된다 라고 생각했던것 같다.
관심사 분리에관한 글 을 이해한 바에따르면
이전 코드의 문제점
하나의 큰 hooks를 생성함으로써 오히려 hooks의 관리가 어려워지는 문제점이 생겼다.
4.수정된 코드
기존 use-feteh-data.js를 기능에 따라 5가지의 hooks로 분리했다.
export const useURLManipulator = () => {
const [params, setParams] = useSearchParams()
useEffect(() => {
setParamValues({ page: 1, take: 10, limit: 10 })
}, [])
/**
* @description query key:value쌍을 반환
*/
const getParamValues = () => {
return {
take: params.get(KEY.TAKE),
page: params.get(KEY.PAGE),
limit: params.get(KEY.LIMIT),
}
}
/**
* @description query key에 값을 선언
*/
const setParamValues = ({ page, limit, take }) => {
params.set(KEY.PAGE, page)
params.set(KEY.LIMIT, limit)
params.set(KEY.TAKE, take)
setParams(params)
}
return { getParamValues, setParamValues }
}
export const useFetchPageNation = ({ path }) => {
const [pageNation, setPageNation] = useState()
const { getParamValues } = useURLManipulator()
const { page } = getParamValues()
useEffect(() => {
fetchPageNationByPath({ path })
}, [page])
/**
* @paramter path - string : "posts" | "comments"
*/
const fetchPageNationByPath = async ({ path }) => {
const response = await axios.get(`/api/${path}`, {
params: getParamValues(),
})
setPageNation(response.data.PageNation)
}
return {
fetchPageNationByPath,
pageNation,
}
}
위의 코드들 처럼 각기능에 따라 hooks를 생성하고 기존page컴포넌트에 있던 useEffect,clickevent 들을 따로 관리해주었다.
5.결과물
const PostDetailPage = () => {
const { postDetail } = useFetchDetail()
const {
commentList,
onClickMoreComments,
onClickHiddenComments,
isOpenCommentList,
} = useFetchComment()
if (!postDetail) return
return (
<div>
<h1>Post Detail Page</h1>
<div>
<p>제목: {postDetail.title}</p>
<p>내용: {postDetail.content}</p>
{!isOpenCommentList ? (
<button onClick={onClickMoreComments}>댓글 보기</button>
) : (
<>
<button onClick={onClickHiddenComments}>댓글 숨기기</button>
{commentList?.map((comment) => (
<div key={comment.id}>
<p>{comment.content}</p>
<p>{comment.User.nickName}</p>
</div>
))}
<PageNation path="comments" />
</>
)}
</div>
</div>
)
}
export default PostDetailPage
그 결과 위의 코드처럼 pageComponet에서는 hooks에서 받아온 state하나만로 fetching을 진행하고 그외의 clickevent, useEffect등은 관리할 필요가 없어졌다.
기존의 코드에서는 pageComponent에서 60~70줄에 걸쳐 작성되어 있던것들이 관심사 분리를 통해 오직 view만 관리하도록 코드를 수정할 수 있엇다.
관심사 분리의 시작을 큰 관점에서 본다면 View vs Logic
을 나누눈 것부터 시작이 아닐까해서 이 글을 작성해보았다. 아직 실력이 없어 두서없이 써내려갔지만
관리할 point를 줄이고 확장성을 높이는 것이 관심사 분리아닐까 생각이 든다.
계속해서 맞이할 상황이기에 다음에 더 좋은 내용으로 작성해야겠다.