코드를 보았을 때 해당 코드가 어떤 페이지를 렌더링 하는 지 한번에 알 수 있어야 좋은 코드라는 이야기는 익히 들어 알고 있었다. 하지만 이걸 어떻게 분리해야 하는지 그동안 감을 잡지 못한 상태였다.
당장 구현만 하는 것도 힘든데… 여기서 어떻게 깔끔한 코드를 만들지? 시간에 쫓겨 구현에만 집중하던 찰나, 요한 멘토님과의 커피챗이 해결책이 되었다.
멘토님이 해결해주신 해결책은 다음과 같다.
Context API 와 커스텀 훅 사용!
기존의 코드는 상위 컴포넌트(부모 컴포넌트) 에서 데이터를 관리하고, 해당 데이터를 필요로 하는 자식에게 내려주는 방식이었다.
const App = () => {
const [data, setData] = useState();
return (
<Child data={data} setData={setData} />
)
}
const Child = ({data, setData}) => {
const handleChange = (event) => {
setData(event.target.value);
}
return (
<input value={data} onChange={handleChange} />
)
}
하지만 알다시피 이 방식은 Child 의 depth 가 증가하면 prop drilling 현상이 발생하기 쉽고, 공통적인 데이터를 prop 으로 일일이 전달하면 코드의 가독성이 떨어지는 문제가 생긴다.
const App = () => {
const [data, setData] = useState();
return (
<Child1 data={data} setData={setData} />
<Child2 data={data} setData={setData} />
<Child3 data={data} setData={setData} />
<Child4 data={data} setData={setData} />
// ...
)
}
이런 현상을 방지하기 위해 Context 로 데이터를 관리해주기로 했고, 비동기 통신 처리를 쉽게 할 수 있도록 react-query 와 병행하여 사용하기로 했다.
우선, 같은 라이프 사이클을 가진 데이터를 하나의 Context 로 묶는다. 여기서 주의해야할 것은 라이프 사이클이다.
같은 라이프 사이클을 가진 데이터를 하나의 단위로 만들어야 Context API 의 단점 (구독한 컴포넌트는 모두 렌더링 된다) 을 최소화시킬 수 있다.
어떤 데이터를 내려줄지 결정했다면 데이터와 데이터를 조작하는 메서드들을 훅으로 만들어 분리하고, 이 훅에서 선언한 함수들을 Provider 로 내려보내준다.
클린 코드를 프로젝트에 적용해보고 싶었기에 팀원들과 상의를 하고 이번 프로젝트에서 렌더링 구문과 비즈니스 로직을 분리하기 위한 방법으로 Context API 와 커스텀 훅을 적극적으로 사용하기로 했다.
이렇게 로직을 분리하는 데 필요한 파일은 다음과 같다.
1. Context.tsx
컨텍스트와 Provider 를 사용하여 전역적으로 관리할 값을 설정하는 파일이다.
2. 커스텀훅.tsx
컨텍스트에서 설정한 값으로 컴포넌트에서 사용할 메서드들을 커스텀 하는 파일이다.
Context 를 사용하기 위해선 createContext 로 Context 를 생성해준다. 이 때 매개변수론 초기값이 들어간다.
초기값은 Provider 로 보낼 데이터를 넣어주면 된다.
const Context = createContext();
const Provider = ({children}) => {
const [data, setData] = useState();
return (
<Context.Provider value={{data, setData}}>
{children}
</Context.Provider>
)
}
export { context, Provider };
context 를 사용할 컴포넌트를 Provider 로 감싸준다.
import { Provider } from './contexts';
const App = () => {
return (
<Provider>
<Child />
</Provider>
)
}
이렇게 함으로써 Provider 로 감싼 하위 컴포넌트들은 모두 data 와 setData 에 접근할 수 있다.
data 와 setData 를 직접 조작하여 필요한 메서드들을 생성하는 것은 모두 커스텀 훅 파일에서 이뤄진다.
import { useContext } from 'react';
import { Context } from './contexts';
const useApp = () => {
const { data, setData } = useContext(Context);
// 데이터 추가 메서드
const addData = (newData) => {
const nextData = [...data];
nextData.push(newData);
setData(nextData);
};
// 데이터 삭제 메서드
const deleteData = () => {
const nextData = [...data];
nextData.pop();
setData(nextData);
};
return {data, addData, deleteData};
}
이렇게 생성한 메서드들을 컴포넌트에서 다음과 같이 사용하면 된다.
import useApp from './hooks';
// ...
const Provider = ({children}) => {
const { add, addData, deleteData } = useApp();
return (
<Context.Provider value={add, addData, deleteData}>
{children}
</Provider>
)
}
import useApp from './hooks';
import { Context } from './context';
const Child = () => {
const context = useContext(Context);
const { add, addData, deleteData} = context;
return (
<div>
{ data }
<button onClick={deleteData}>삭제</button>
<button onClick={addData}>추가</button>
</div>
)
}