리팩토링 일지: 커스텀 hook으로 자식 컴포넌트의 props 묶기
커스텀 hook으로 자식 컴포넌트의 props 묶기
커스텀 hook으로 동일한 자식 컴포넌트에서 사용되는 props를 묶어서 코드의 유기성을 높였다.
왜 커스텀 hook을 사용했는가?
fetch함수를 부모 컴포넌트에서 선언하면 받아온 data를 자식들에게 props로 내려주어야 한다. 그러나
그러나 나는 API fetch 함수를 컴포넌트 내부에 독립적으로 선언하고 useEffect로 호출하도록 코드를 작성했다. 아래의 코드는 검색 키워드와 사용자가 선택한 카테고리로 검색 결과를 요청하는 API를 fetch한 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// API fetch 함수 선언
const getCategorizedSearchingData = useCallback(async () => {
const res = await getDestinationListByTitleAndCategoryId(
selectedCategory,
searchQueryParam
);
const totalData = res.data.total_count;
...
}, [
selectedCategory,
searchQueryParam,
setFilteredDestinations,
isUserSearched
]);
// API fetch 함수 호출
useEffect(() => {
getCategorizedSearchingData();
}, [getCategorizedSearchingData]);
고민했던 점
- 동일한 fetch 함수를 호출하는 사용자의 action이 둘 이상인 경우, 어떻게 커스텀 hook으로 묶을 것인가?
- 같은 state를 사용하는 두 컴포넌트를 어떻게 커스텀 hook으로 분리할 것인가?
- 커스텀 hook에 인자로 주는 방식 (참고)
- contextAPI를 사용한다.
- 첫번째 아이디어
- Search와 Category 컴포넌트의 부모 컴포넌트 destinationsFilter를 새롭게 생성
- 두 컴포넌트에 필요한 props들을 하나의 부모 컴포넌트에 모두 작성하여 코드가 복잡하고 가독성이 떨어진다.
- 두번째 아이디어
- Search, Category를 각각의 커스텀 훅으로 분리하고, 필요한 인자를 전달하는 방식
- Search에서 관리하는 state와 Category에서 관리하는 state를 서로 필요로 하기 때문에 커스텀 훅 간의 관계가 복잡해지고 코드의 흐름을 이해하기 어려워진다. 이에 따라 다른 방법을 고민했다.
- 세번째 아이디어
- 목적지 리스트 fetch 함수는 반드시 검색 키워드와 선택한 카테고리 목록을 인자로 필요로 함. 따라서 ContextAPI를 이용하여 인자로 필요한 state를 Context로 지정한 후 사용자의 action 함수 내부의 목적지 리스트 fetch 함수에 전달될 수 있도록 수정
검색 키워드의 경우,
useSearchParams훅을 이용하여 쉽게 얻을 수 있다.이 아이디어를 채택하면 선택한 카테고리 목록을 Context 값으로 전달하게 된다. 그러나 Search, Category 컴포넌트에 이를 Context 값으로 전달하는 것이 코드 흐름상 어색하게 느껴진다. 의도대로 동작은 하겠지만 개인적으로 처음 이 코드를 보는 사람이 이러한 구조를 이해할 수 있을지 의문이 들기 때문에 보류했다.
- 네번째 아이디어
- Search 컴포넌트의 목적지 리스트 fetch 함수에 선택한 카테고리 목록 대신, 반드시 전체 카테고리 목록을 사용하도록 리팩토링하여 Search 컴포넌트와 Category 컴포넌트가 공통적으로 사용해야 하는 props를 제거
내가 커스텀 훅으로 코드를 분리하는데 어려움을 느낀 이유는 서로 다른 역할을 하는 두 컴포넌트가 동일한 props를 필요로 했기 때문이다. 따라서 두 컴포넌트가 공통적으로 사용하는 props를 대체할 수 있는 방법을 고민했다.
Search 컴포넌트에서 실행되는 목적지 리스트 fetch 함수에 ‘사용자가 선택한 카테고리 목록’에 대한 state가 꼭 필요한 지 고민했다. Search 컴포넌트에 정의된 fetch 함수는 사용자가 검색 키워드를 제출하는 action의 영향범위에 포함되어 있다.
일반적으로 ‘결과 내 검색’이 아니라면, 사용자가 검색했을 때 페이지가 리렌더링되거나 기존 검색결과가 초기화 되면서 선택한 카테고리 필터도 초기화 된다. 따라서 사용자가 선택한 카테고리 목록 없이 검색 키워드만을 이용하여 서버에 목적지 리스트 데이터를 요청해도 된다는 사실을 깨달았다.
이에 따라 Search 컴포넌트의 목적지 리스트 fetch 함수는 요청에 필요한 카테고리 인자에 항상 전체 카테고리를 넣도록 수정하였다.
고찰
리팩토링 전에도 검색 키워드를 제출할 때마다 기존에 선택한 카테고리가 ‘전체’로 초기화 됐었다. 그런데도 내가 이 사실을 간과하고 계속 Search 컴포넌트에 사용자가 선택한 카테고리 목록을 전달하려 했던 이유에 대해 생각해보았다.
첫번째로, 내가 기존에 작성한 코드만 보면서 리팩토링을 시도했기 때문이라고 생각한다.
페이지가 동작하는 흐름을 다시 한 번 직접 보면서 리팩토링을 고민했더라면 내가 이상한 방향으로 가고 있었다는 사실을 빨리 알 수 있었을 것이다.
두번째로, 기존의 코드가 영향범위를 파악하기 어렵게 작성되어 있었기 때문에 검색어를 제출하는 action이 기존에 선택된 카테고리를 초기화한다는 사실을 고려하지 못했다. 나는 Search -> Category -> Destination 순으로 props를 전달하는 방식으로 코드를 작성했었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Search
function Search{
return (
...
<Category
{...props}
/>
...
)
}
//Category
function Category{
return (
...
<Destinations
{...props}
/>
...
)
}
즉, Category, Destinations의 렌더링을 정의한 곳이 다른 기능을 수행하는 컴포넌트 내부였기 때문에 전체적인 페이지 구조를 파악하기 어렵고 코드가 잘 눈에 띄지 않았다. 그 결과 Search 컴포넌트가 리렌더링 될 때 자식 컴포넌트인 Category도 같이 리렌더링 되면서 선택한 카테고리 목록이 초기화된다는 사실을 코드상으로 눈치채지 못했다.
이는 코드를 선언적으로 작성하지 않아서 생긴 문제라고 생각한다. 이번 리팩토링을 통해 선언적인 코드 작성이 유지보수 관점에서 왜 중요한지 체감되는 경험을 한 것 같다.