벌써 새 회사에 온지 9개월이 다 되어간다. 처음 회사에 와서 코드를 접했을 때, 예상은 했지만 레거시가 예상보다도 훨씬 심각했다. react 16버전, node 14버전에 redux, CRA 등 기술 스택이 4년전에 멈춰있었던 것도 심각한 문제였지만, 더 심각한 문제는 컴포넌트의 관심사 분리가 전혀 되어있지 않았다는 점이였다. 커스텀 훅도 전혀 없는 상태였고, 그냥 데이터 페칭, 계산, view가 모두 한 컴포넌트에 버무려져 있었고, 가장 심각한 경우 한 컴포넌트가 2000줄이 넘는 케이스도 있었다.
지난 9개월 동안은 급한 버그 수정, 다른 프로젝트, 그리고 리뉴얼을 진행하면서 이런 낙후된 코드 환경을 개선하지 못했었지만, 최근에 대규모 리뉴얼 작업이 끝나면서 조금 여유가 생겼고 이제 그동안 알면서도 외면해왔던 끔찍한 코드들의 리팩토링을 진행해야겠다고 마음먹게 되었다.
하지만 제대로 마음먹고 개선하려고 하자, 생각보다 어려움이 많이 있었다. 우선 내가 관심사를 분리하는 방법에 대해서 명확하게 알고 있지 못했다는 점을 깨닫게 되었다. 신입 개발자들도 SOLID 원칙, 비즈니스 로직과 View 로직을 분리해야한다는 이론 정도는 다 알고있을 것이다. 당연히 나 또한 그랬지만, 실제로 이것을 컴포넌트를 설계할 때 적용할 수 있는지는 다른 차원의 문제였다. 게다가, 관심사가 전혀 분리되지 않은 나쁜 코드를 보며 오랫동안 작업하다보니 더욱 이런 원칙을 적용하는데서 멀어지게 된 것 같다.
더이상 신입 개발자도 아닌 만큼, 나만의 명확한 기준을 갖고 컴포넌트를 설계해 나갈 수 있어야 겠다는 생각이 들어서 이번 기회에 제대로 각잡고 공부해보기로 했고, 다양한 소스를 통해 여러가지 방법이 있음을 알게 되었다.
매우 전통적인 리액트의 관심사 분리 방법이다.
이렇게 불리기도 하는 것을 보면 명확하게 알 수 있듯이, Presentation 컴포넌트에는 상태가 있으면 안되고, 데이터 페칭, 계산 등은 반드시 Container 컴포넌트에 위치시켜야 한다. 그래서 Presentational 컴포넌트는 정해진 구조의 데이터만 받도록하여 dumb하게 화면에 그려주는 역할만 하는 것이고, 그렇기 때문에 어디서 사용하던지에 관계없이, 'props를 통해' 정해진 구조의 데이터를 넘겨받기만 하면 재사용하기도 쉬운 것이다.
그러나 이 패턴은 React 16버전부터 추가된 hooks에 의해 대체되어 잘 사용되지 않는다고 알려져있다.
Dan Abramov가 본인의 블로그 글 Presentational and Container Components 에 추가한 글
2019년 업데이트: 이 글은 오래전에 작성된 것이며, 제 관점은 그 이후로 변화했습니다. 특히, 이렇게 컴포넌트를 분리하는 것을 더 이상 권장하지 않습니다. 만약 코드베이스에서 자연스럽게 느껴진다면, 이 패턴은 유용할 수 있습니다. 하지만 불필요하게 강제되고, 거의 독단적인 열정으로 적용되는 경우를 너무 많이 보았습니다. 제가 이 패턴이 유용하다고 느꼈던 주된 이유는 복잡한 상태 관리 로직을 컴포넌트의 다른 측면과 분리할 수 있었기 때문입니다. 훅(Hooks)을 사용하면 이러한 임의의 분리 없이도 같은 작업을 할 수 있습니다. 이 글은 역사적인 이유로 그대로 남겨두었지만, 너무 심각하게 받아들이지 마세요.
이 글을 보면, 마치 Presentational / Container 컴포넌트 패턴은 레거시하며, 더이상 유용하지 않은 경우 가 많은 것처럼 여겨진다. 하지만 나의 해석은 완전히 다르다. 해당 패턴은 여전히 유효하지만, Container 컴포넌트의 역할이 크게 달라졌다는 것을 의미할 뿐이라고 생각한다. 이어서 왜 Hooks의 도입이 이런 변화를 불러일으켰는지 설명하려한다.
더이상 hooks는 새로운 개념이 아니기 때문에 설명은 필요없는 것 같고, hooks를 도입 했을 때와 도입하지 않았을 때 Container 컴포넌트가 어떻게 달라지는지 간단한 예제를 통해 비교해보자.
const UserContainer = () => {
const [users, setUsers] = useState([])
useEffect(() => {
fetchUsers().then(setUsers)
}, [])
const handleDeleteUser = (id) => {
// 삭제 로직
}
return <UserPresentation users={users} onDelete={handleDeleteUser} />
}
const UserContainer = () => {
const { users, handleDeleteUser } = useUsers()
return <UserPresentation users={users} onDelete={handleDeleteUser} />
}
// custom hook
const useUsers = () => {
const [users, setUsers] = useState([])
useEffect(() => {
fetchUsers().then(setUsers)
}, [])
const handleDeleteUser = (id) => {
// 삭제 로직
}
return { users, handleDeleteUser }
}
custom hook을 통해 기존에 Container 내부에 위치하던 비즈니스 로직을 Container 컴포넌트 밖으로 분리할 수 있게 되었다.
그렇기 때문에 Container 컴포넌트는 원래 비즈니스 로직을 담당하는 역할에서, 비즈니스 로직과 View 로직을 이어주는 중간 매개체 연결로 축소된 것이다.
그래서 내가 내린 결론은, Container / Presentational 패턴은 여전히 유효하며, hooks 도입 이후에 Container 컴포넌트의 역할이 축소되었을 뿐이라는 것이다.
그래서 기존의 Container / Presentational 패턴에 hooks를 얹어서 다음과 같은 3계층 구조로 관심사를 분리하는 것이 현 시점에서의 best practice라고 정리할 수 있을 것 같다.
Custom Hooks (비즈니스 로직)
Container 컴포넌트 (연결 계층)
Presentational 컴포넌트 (UI 로직)
claude에게도 물어보고, 여러 블로그 글, 유튜브 영상, 시니어 개발자분께도 물어보면서 컴포넌트 관심사를 분리하는 기준을 추상적으로 이해하게 될 수 있었고, 이것을 좀 더 구체화해서 명확하게 글로 정리해보게 되었다.
이렇게 정리한 이론을 회사 코드의 한 부분에 적용해보았는데, 정말 놀랍게도 코드를 파악하는데 드는 시간이 크게 줄었다는 것이 확 체감되었고, 특히 Presentational 컴포넌트를 명확하게 분리하고 나니 기존에는 재사용이 어려웠던 모달 컴포넌트를 매우 간결하게 재사용할 수 있게 되어 매우 만족스러웠다. 이제서야 관심사를 분리하는 명확한 자신만의 기준을 찾은 것 같아서 마음이 후련하다.
그리고 여러 자료를 조사하고 공부하면서 함수형 프로그래밍의 액션, 데이터, 계산으로 분리하는 방식에 대해 큰 흥미가 생기게 되었는데, 쏙쏙 들어오는 함수형 코딩이라는 유명한 책을 사서 공부해보려고 한다.
다음에는 이 책에서 배운 내용을 정리하고, 또 실제로 적용하면 어떻게 관심사를 더 명확하게 분리하는데 도움이 되는지에 대해서도 포스팅 해보아야겠다.
관심사가 전혀 분리되지 않은 코드에서 오랜 시간 작업하다 보니, 이러한 원칙을 적용하는 것에서 점점 멀어졌다는 말에 깊이 공감합니다.
저도 원칙을 알고는 있지만, 실제로 적용하려고 하면 쉽지 않아서 결국 기존 코드 패턴을 그대로 따라가게 되는 경우가 많았던 것 같습니다.
또한, Container-Presentational 패턴이 이제는 레거시한 방식이고, 더 이상 사용하면 뒤처지는 것이라는 인식이 강했는데,
동현님 덕분에 새로운 시각을 갖게 되었습니다. 문제를 접근하는 방식이 정말 뛰어나신 것 같아요! 좋은 인사이트 얻어갑니다. 😊