💡 프리온보딩 프론트엔드 챌린지 8월의 첫번째 수업 내용을 기반으로 리팩토링한 내용을 기록한 글입니다.
사전과제 투두리스트 Github 링크
TodoItem.tsx 이란 컴포넌트는 투두 리스트에서 하나의 todo 아이템을 나타내는 컴포넌트 입니다. 해당 컴포넌트 안에는 투두 아이템 수정, 삭제, 상세조회 기능이 들어가 있습니다.
처음에는 하나의 컴포넌트 안에 모든 비즈니스 로직(수정, 삭제, 상세조회), UI (input, button 및 관련 상태) 에 관련된 코드를 모두 작성했습니다.
이렇게 작성할 경우 지금은 간단한 TodoItem 이기 때문에, 함수를 클릭해가며 동작 순서를 파악하기 쉽지만, 코드가 조금만 길어지고 복잡해질경우 동작 과정을 직관적으로 파악하기 어렵다는 문제점이 있었습니다. 제가 생각한 제 코드의 문제점(?) 은 아래와 같습니다.
이렇게 상세조회, 수정, 삭제 에대한 hooks (react query useMutation 호출함수) 이 섞여져 있습니다.
여러가지 동작을 한 파일에서 관리하니 Button 별로 수행하는 역할이 직관적으로 파악이 어려웠습니다.
TodoItem 이라는 파일 내에 여러 목적의 함수가 함께 작성되어 있어 동작 순서나, 원리를 파악하기 힘듭니다. 또한 각 함수마다도 버튼이 클릭됐을 때 발생하는 이벤트들을 한꺼번에 작성해놓으니, 구체적인 세부 구현이 한눈에 들어오지 않습니다.
한번에 한 가지만 걱정해도 괜찮도록' 각각의 관심사에 따라 코드를 분리하는 기법
제가 처음에 작성한 코드처럼 하나의 함수, 변수, 컴포넌트에게 한번에 너무 많은 일(concerns)을 부여하게 되면 코드를 읽는 사람은 혼란스러울 수 있습니다.
이를 해결하기 위한 방법은, 한번에 한 가지 걱정만 할 수 있도록 단위를 잘게 나누는 것입니다. 즉, 코드는 단위별로 하나의 관심사만 갖게 하고 그 관심사에 대해서만 충실히 동작하도록 만드는 것입니다.
이렇게 되면 전체 기능을 파악하기 위해 일겅야 하는 코드 단위가 줄어들고, 분할-정복(Devide & Conquer) 방식의 코드 파악이 용이해집니다.
관심사의 분리가 적절히 구현된 코드에서는
과 같은 특성을 발견할 수 있습니다.
처음 코드를 짤때는 나름 react query 커스텀 훅도 만들고, input 상태값이나 동작을 관리하기 위한 useForm 커스텀 훅도 만들어 가독성이 좋은 코드를 만들어보려 노력했지만, 진유림님의 클린코드 강의나 여러 자료를 참고하고 다시 보니 문제점이 많은 코드였습니다.
TodoItem 컴포넌트가 여러가지 기능을 구행하고 있었고 (낮은 응집도), 각각의 기능을 수행하는 버튼은 클릭이벤트와, 상태값이 노출되어 있어 역할을 직관적으로 파악하기 어려웠습니다. (추상화 안되어 있음). 또한, 몇몇 함수는 한번에 여러가지 역할을 수행하도록 짜여져 있었기 때문에 재사용이 어렵고, 다른 사람이 봤을 때 이해하기 어렵다고도 생각했습니다. (단일 책임 깨짐)
우선 TodoItem.tsx 파일 전반에 수정, 삭제, 상세조회를 목적으로 하는 코드가 흩어져서 작성되어 있던것을 쪼개고, 각각의 기능으로 분리하기 위해 UpdateButton, DeleteButton, DetatilButton 이라는 컴포넌트를 분리했습니다.
아래와 같이 수정에 대한 기능을 한 군데에 뭉쳐두니, 기능 파악도 쉽고 추후 변경에도 용이한 컴포넌트로 개발할 수 있었습니다.
또한, 각각 기능에 필요한 hook 이나 type, 스타일 컴포넌트를 따로 import 해올 수 있었기때문에 코드 가독성이 좋아지는 효과도 얻을 수 있었습니다.
다음으로 수정, 삭제, 상세조회 동작을 수행하는 Button 을 추상화 시켜보았습니다. 각각 버튼의 구체적인 상태값이나, UI 구현 방법, 동작 과정을 추상화하고 컴포넌트 이름만으로도 각각의 버튼이 어떤 역할을 수행하는지 직관적으로 파악할 수 있도록 수정했습니다.
여기서 각 버튼이 공유해야할 state 들이 몇가지 있었습니다. (수정 모드를 나타내는 isEdit state 등 .. ) 해당 state 들은 가장 가까운 부모 컴포넌트인 todoItem 으로 끌어올려, 공통 속성은 한군데서 관리될 수 있도록 작성했습니다.
여기서 사실 컴포넌트 간 공유하지 않아도되는 상태값은 각 컴포넌트 안에 숨겨 추상화를 시키고 싶었는데,,저의 구현능력 부족으로 몇가지 상태는 숨기지 못했습니다.
마지막으로 하나의 함수가 여러 역할을 수행할 경우, 각각의 동작을 명시적으로 나타내는 함수이름으로 바꾸고 함수마다 오로지 하나의 역할만을 수행할 수 있도록 분리했습니다. 아래 onClickShowDetailButton 은 todo item 의 상세정보 조회 버튼 [상세] 를 클릭했을 때 발생하는 이벤트를 실행시키는 함수입니다. 리팩토링 이전의 코드에서는 1)상세조회 토글 버튼을 닫는 역할과, 2) 상세정보를 조회한 todo item 의 정보를 search param 에 저장하는 역할을 함께 수행하고 있습니다.
지금은 간단한 기능 2가지만 수행하고 있기 때문에, 코드를 읽고 이해하는데 큰 무리는 없지만 만약 상세 조회 버튼을 클릭했을 때 발생해야 하는 이벤트가 다양해지고, 새로운 기능이 추가된다면 저 onClicckShowDetailButton 함수는 끝없이 길어질 수 있습니다. 이런 함수는 나중에 유지하수하기도 어렵고, 다른사람이 읽고 이해하기도 어려워집니다.
이런 상황을 미연에 방지하기 위해 해당 함수에 단일 책임 원칙을 적용해봤습니다. 상세조회 토글 열고 닫기를 제어하는 부분은 changeDetailOpen 함수로, search parameter 에 todo id를 저장하는 부분은 changeSearchParams 을 분리했습니다.
이렇게 onClick 이벤트에 해당 함수를 명시적으로 선언함으로써, 버튼이 클릭됐을 때 어떤 동작이 수행되는지 직관적으로 파악할 수 있도록 수정했습니다.
위의 과정을 거쳐 리팩토링이 끝난 TodoItem 컴포넌트의 모습니다. Input 상태값을 관리하는 로직은 useForm 안에, 공통으로 사용하는 상태값인 isEdit, isOpen 등을 제외한 나머지 세부구현은 각각의 역할을 담당하고 있는 컴포넌트로 추상화해 숨겼습니다.
원티드 프리온보딩 코스의 사전과제로 작성항 투두리스트 프로젝트에 평소 이론적으로만 알고 있었던 관심사 분리 원칙을 급하게(?) 적용해봤습니다. 가장 어려웠던 점은 서로 의존성 있는 컴포넌트일 경우, 결합도를 끊고 각각의 컴포넌트로 추상화하는 부분이었습니다. 예를 들어, 수정의 경우, input 부분과, submit 부분에 같은 inputValue 를 넘겨줘야 하는데 각각의 컴포넌트로 추상화하면서 공통 value 를 깔끔하게 관리하는 부분이 어려웠습니다.
또 추상화를 시키는 과정에서, 차마 좋은 인터페이스를 생각하지 못하고 밖으로 드러낸 state 들이 꽤 많습니다. 앞으로 좋은 컴포넌트 설계기법에 대해 더 많은 공부와 고민이 필요하단 생각이 들었습니다. 다음 리팩토링으로는 타입스크립트와 관련해 any, 타입 단언 없애기, 타입 가드 및 추론으로 타입 좁히기 를 꼭 적용해보려고 합니다!