관심사분리(Logic vs View)

박채윤·2024년 3월 22일
2

관심사 분리

목록 보기
1/1

들어가면서...

이번과제에는 sphagettie code Refactor를 진행했다.

  • 👇 해당 과제의 내용은 다음과 같다.
    • 게시글 & 댓글 목록을 보여주는 페이지이며 pagination 기능이 구현된 페이지이다.
    • 게시글, 댓글, pagination api요청이가능하며 모달창을 통해 페이지를 이동하도록 작성되어있다.

😱문제점

  • sphagettie code를 열어보니 아래와같았다.
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를 모두 관리하는것이 문제처럼 보여진다.

😎설계

간단하게 코드의 기능과 문제점을 살펴봤으니 해결방법을 생각해보자.

    1. 재사용되는 컴포넌트를 하나의 컴포넌트로 합치기.
    1. 각기능에 맞는 logic들을 별도의 파일로 분리해서 관리하기.
    1. 너무많은 state와 useEffect를 가지고있으니 따로분리하기.

다음과 같이 Refactor 를위해 설계를 하고 진행해보자.

👏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함수를 생성해서 기능들을 빼내는것에는 성공을 했는데 .....
큰문제를 발견했다.

  • use-fetch-data를 각 컴포넌트들에서 불러와 사용하니 기존코드보다 관리, 재사용적인 면에서 refactor가 진행됬다고 보여지는데
/ 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",
    })
  }
  • 위와 같이 hook함수를 이용해 관심사분리를 했지만
  • 가독성은 여전히 refactor전과 달라진게없다.
  • 또한 패칭을위한 UseEffect 또한 너무많이 선언되어있다.

3.👀관심사 분리

방향이 잘못된거같아 검색을 하던중 좋은 글을 발견했다.
관심사 분리에관한 글 을 읽어보고 다시 방향을 잡을 수 있엇다.
☝ 재밌는 내용이니 읽어보세요~~

내가 생각했던 관심사 분리란 view를 담당하는 컴포넌트에서 logic을 추출 하기만 하면 된다 라고 생각했던것 같다.

관심사 분리에관한 글 을 이해한 바에따르면

    1. 기능과 view를 분리할 것
    1. 기능들 또한 관심사 에 따라 분리할 것
    1. 분리된 기능들은 사용 목적에 따라 관리 할 것

이전 코드의 문제점
하나의 큰 hooks를 생성함으로써 오히려 hooks의 관리가 어려워지는 문제점이 생겼다.

4.수정된 코드

기존 use-feteh-data.js를 기능에 따라 5가지의 hooks로 분리했다.

  • use-url-manipulator.js
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 }
}
  • use-fetch-pagenation.js
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를 줄이고 확장성을 높이는 것이 관심사 분리아닐까 생각이 든다.
계속해서 맞이할 상황이기에 다음에 더 좋은 내용으로 작성해야겠다.

profile
왕이될 상인가

0개의 댓글