리액트 프로젝트를 리팩토링해보자 - 1편

Ben·2022년 9월 9일
1

Today I Learned

목록 보기
48/57

팀 프로젝트 그 후

데브코스에서 진행했던 1차 팀 프로젝트 공식 기간이 끝나고, 대부분 프로젝트 경험이 좋았는지 꾸준히 프로젝트를 개선하고 싶다고 하셔서, 앞으로 프로젝트 방향에 대해 고민하는 시간을 가졌다. 실제로 방향성에 대해서 고민한 시점은 팀 프로젝트가 끝난 직후인 6월 말?이긴한데, 구체적인 이야기가 나온 시점은 이번주이기 때문에 현 시점에서 정리를 하게 되었다.

앞으로의 방향성

1차 스프린트 후 2차 스프린트 전에 무엇을 할 지 팀원들과 함께 이야기를 해 보았다. 첫 번째 나왔던 내용은 시간이 없어서 원하는 기능들에 대한 시도를 못했기 때문에 새로운 기능 구현을 하고 싶다는 의견이 있었다. 그리고 다른 의견으로는 현재 프로젝트 구조 상 기능 추가가 어렵기 때문에, 우선 리팩토링을 통해 프로젝트 구조를 정리 후, 새로운 기능을 추가하자는 의견이었다.

프로젝트 구조를 보면, 다들 구현에 급급하여 여러 레이어가 뭉쳐있는 상태이다.

그러므로, 새로운 기능 추가를 할 수 있는 공간이 없기 때문에 누가 봐도 현재 상태에서 기능 추가를 하는 것은 무리라고 생각했다. 따라서 리팩토링을 이번 스프린트 목표로 잡았다.

리팩토링 전

리팩토링을 하기 전, 의미 있는 리팩토링 스프린트가 되도록 각자 생각하는 리팩토링의 방향성과 현재 작성했던 코드의 문제점에 대하여 생각하는 시간을 가졌다. 또한, 이번 리팩토링 스프린트 전에 컨벤션과 룰을 새롭게 정의하고 가야할 듯 하여, 폴더 및 파일구조와 여러 lint convention 에 대하여 제안할 것이 있다면 제안하자고 했다.

체퀴즈 리팩토링 진행 방향

내가 생각하는 리팩토링의 방향

사실 요즘 인상 깊게 보고 있는 내용이 layered architecture와 관련된 내용이다. 해당 내용은 나중에 좀 더 자세히 글을 작성하고자 한다. 간략하게 말하자면, 계층을 나누어 계층간 응집력은 높이고, 각 계층간 결합도를 낮추는 것이다. 어떻게 보면 SOLID 원칙과 굉장히 유사하다.

크게 3가지 레이어로 나누어져있다. 리스트 기준 오름차순으로 의존성이 높아지고 변경 사항이 많다.

  • data access layer: 데이터의 인터페이스 및 엔티티 정의
  • domain layer: 각 도메인의 비즈니스 로직 레이어
  • presentation: 실제 프레임워크와 연결되어 있는, 가장 의존성이 높고 변화 가능성이 큰 레이어

우리 프로젝트도 이런 3단계의 구성 방식을 통해, 각 층에서의 변화를 줄이고 유연하게 대처할 수 있지 않을까 해서 고민하고 제안할까 했었다.

그러나 현재 프로젝트에서 그렇다 할 만한 비즈니스 로직이 많지 않고, 또 엔티티를 활용하는 경우도 없다. 단순히 타입스크립트 인터페이스 정의와, 서버로부터 api 요청을 통해 데이터를 받아오는 부분이 크다.

레이어를 철저히 나누는 과정이 현재로서는 오버 엔지니어링이라고 생각하여, 해당 부분은 당장 도입하지 않기로 했다. 다만, 최대한 각 영역에서 의존성을 줄이는 것은 중요하기 때문에, 독립적인 도메인을 유지하는 방향 자체는 지향할 것이다.

그래서 두 번째로 제안한 것이 headless하게 컴포넌트를 구현하는 것이다.

[ Headless란? ]

컴포넌트에서 제어하기 어려운 UI 부분을 없앤다. 즉, 로직과 UI를 완벽히 분리하여 변경에 유연한 컴포넌트를 만든다.

컴포넌트를 바라보았을 때, 세 가지 관점으로 추상화 할 수 있다.

  • 데이터 관점
  • 이벤트 관점
  • UI 관점

이렇게 각각 추상화했을 때 남는 것들로 분리하여 단일 책임 원칙에 맞는 컴포넌트를 만드는 것이다.

또한, 단순 분리 차원으로 끝나는 것이 아닌, 각 컴포넌트 (데이터, 상호작용, UI) 모두 각각 SOLID 원칙을 준수해야 한다. 모두 작은 단위를 유지해야, 변경이 발생한 경우에도 해당 변경된 부분만 고치면 되기 때문이다.

[ React 환경에서 headless 컴포넌트를 구현하기 위해서는? ]

SOLID 원칙에 기반한 headless 컴포넌트를 구현하기 위해서, 리액트에서 사용하기 적합한 hooks 패턴을 사용하기로 하였다.

hooks를 사용하면 컴포넌트에서 로직을 재사용할 수 있고, 데이터 메서드들을 hook으로 묶어서 데이터 추상화 관점에서 봤을 때 응집도는 높고, 결합도는 낮게 유지할 수 있기 때문이다. 그래서 hook을 많이 만들고, 컴포넌트에서는 hook을 통해 데이터에 접근하여 데이터를 다루는 메서드가 UI 단에서 정의되도록 하지 않아야한다.

[ React 환경에서 SOLID 원칙 ]

hooks를 이용하여 로직을 컴포넌트 밖으로 뺐다고 SOLID 원칙이 완성되는 것은 아니다. 로직 내에서도 책임이 많은지, UI 역시 하나의 책임을 갖고 있는지 확인하며, 확장에 개방적이고 수정에 폐쇄적인지 확인해야 한다.

재사용성이 높은 코드를 생산하기 위해서는,

  • 코드가 늘 단일 책임 원칙을 준수하는지 확인해야 한다.
  • 로직 또는 UI 컴포넌트에 원하지 않은 도메인이 존재하는지 확인해야 한다.
    • 로직과 UI이 도메인에 묶여있으면 재사용이 어렵다. 반드시 도메인이 존재하지 않는, 제너럴한 컴포넌트를 만들고, 필요시 이를 조합하여 도메인을 넣도록 하자. (리액트에서는 상속보다는 조합을 이용하여 컴포넌트를 만드는 것을 권장한다.)
  • UI의 경우 작게 만들어서 조합을 사용하자.

결론: hooks 패턴을 이용하여 컴포넌트의 로직을 분리하고, 리팩토링하자.

내 코드의 문제점과 해결 방안

리팩토링에 앞서서, 기존에 작성했던 코드에 어떤 문제점이 있고, 위에서 나왔던 단일 책임 원칙으로 어떻게 리팩토링할 수 있는지 알아보자.

  • 기존 코드
    function QuizSolvePage() {
      const history = useHistory();
      const sliderRef = useRef<Slider | null>(null);
      const { user, setUser, isAuth } = useAuthContext();
      const { channelId, randomQuizCount, setChannelId, setRandomQuizCount } =
        useQuizContext();
    
      const [quizzes, setQuizzes] = useState<QuizInterface[]>([]);
      const [userAnswers, setUserAnswers] = useState<string[]>([]);
      const [currentIndex, setCurrentIndex] = useState(0);
      const [loading, setLoading] = useState(true);
    
      const handleUserAnswers = useCallback((index: number, value: string) => {
        // ...
      }, []);
    
      const updateUserPoint = useCallback(async () => {
        // ...
      }, []);
    
      const handleSubmit = useCallback(
        async (e: React.FormEvent) => {
          // ...
        },
        [
           // ...
        ],
      );
    
      // Slider Options
      const settings = useMemo(() => {
        return {
          // ...
        };
      }, []);
    
      useEffect(() => {
        // page logics
      }, []);
    
      // ...
    }

… 그만 알아보자.

우선 이 코드는 몇 가지 문제점을 가지고 있다.

QuizSolvePage의 로직은 다음의 역할들을 한다.

  • 서버로부터 퀴즈 데이터를 불러온다.
  • 사용자가 선택한 답 처리
  • 사용자 점수 처리
  • 사용자가 선택한 답을 서버에 제출
  • react-slick settings 처리

거기다가 UI부분까지 본다면, 이 page 컴포넌트의 역할은 더욱 많다.

[ 해결 방안 ]

각 데이터와 데이터를 다루는 메서드들을 공통된 관심사별로 묶어, custom hook으로 관리한다.

이때, 각 hook이 단일 책임 원칙을 위반하지 않는지 주의한다.

다음은 뭉쳐져있는 로직들을 어떻게 분리할 것인지 대략적인 예시를 적어보았다.

  1. 각 데이터와 데이터를 다루는 메소드를 각각 useQuiz, useUserAnswer, useUserScore 등으로 나눈다.
  2. 정의된 메서드들을 바탕으로 좀 더 도메인에 의존적인 useQuizSolve, useQuizResult hook을 구성하여 각 페이지에서 사용할 수 있도록 한다.

custom hook을 통해 로직이 정리가 된 후, 컴포넌트를 쪼개어 조합하는 방식으로 단일 책임 원칙을 준수한다. 또한 storage에 접근하는 부분은 side effects이기 때문에, 개발자가 해당 부분을 제어할 수 있는지 없는지 다시 한번 확인해야 한다. hook 내부 메서드의 매개변수로 분리할 수 있으면 좋을 듯 하다.

이벤트 함수들을 분리한다.

해당 함수들은 hooks가 아닌 컴포넌트에서 처리한다.

리팩토링 관련 컨벤션 및 룰 새롭게 정의하기

리팩토링에 앞서, 프로젝트의 구조와 컨벤션을 개편해야 할 필요성이 있었다.

기존 프로젝트의 구조는 다음과 같았다.

src
├── api
├── assets
├── components
├── common
├── hooks
├── context
├── containers
├── contexts
├── pages
├── routes
├── styles
└── utils

기존의 구조는 다음의 문제가 있었다.

  • 사용하지 않는 폴더 - containers
  • components 내부 폴더가 너무 복잡 - 도메인에 얽혀 있는 컴포넌트들과 크기가 작은 컴포넌트들이 합쳐져 있기 때문에 너무 지저분하다.
  • common 폴더 역시 사용 빈도가 적고, 여기에는 적혀있지 않지만 foundations 폴더라는, 스타일 상수만 모아 놓은 폴더가 따로 존재하여 둘의 역할이 겹친다.
  • src 폴더 밖에 d.ts 파일의 존재

여러 문제를 해결하기 위해, 팀원들과 의견을 모아 각자 생각하는 컨벤션을 공유하고, 그 중 괜찮은 의견들을 모아서 최종적으로 확정했다.

최종 결정 사항

컨벤션

팀 내에서 사용할 컨벤션을 좀 더 세부적인 부분까지 확정했다. 공식적인 프로젝트 기간에서는 시간 문제 상 큰 틀에서 이렇게 정의하자~ 하고 해당 범위 내에서는 자유롭게 작성하여, 코드가 쌓일 수록 구조가 지저분에 지는 현상이 있었다. 이번에는 디테일한 네이밍 컨벤션과, 함수 정의 방법, 폴더 구조, 타입 지정, CSS 프로퍼티 순서, styled-components(emotion) css 작성 타입까지 컨벤션으로 정의해 두었기 때문에, 앞으로 컨벤션을 지키지 않아 서로가 불편해질 일은 많이 사라질 것이라고 기대한다.

물론 해당 컨벤션이 완벽하게 정의 되었다고 생각하지 않는다. 아직까지 컨벤션으로 정하지 않은 부분이 훨씬 많고, 수정이 되어야 할 부분들도 있다. 이러한 부분들은 프로젝트를 개선하면서 토의를 하면 될 것이라고 생각한다.

PR Rule

기존에는 시간 관계상 리뷰가 끝나지 않고도 머지되는 경우가 있었기 때문에, 해당 경우를 최대한 방지하고자 좀 더 강도 높은 PR rule을 도입했다.

리스트럭처링 기간이 지난 뒤, 파일 수의 변화와 라인 수 변화를 모두 신경써서, 최대한 작은 범위의 수정을 하나의 단위로 잡기로 했다. (컴포넌트 1개의 변화 - 1개의 PR)

또한, 코드 리뷰 과정이 모두의 성장 과정임에 공감하여, 코드 리뷰 in 뱅크샐러드 개발 문화 | 뱅크샐러드 (banksalad.com) 의 코드 리뷰 룰을 가져오기로 했다. 단, 조금 간단하게 가져오기로 했다.

Pn 룰

  • P1 : Request Changes
    • 무조건 반영해야 한다고 생각하는 부분
    • 리뷰이는 반영 후 PR 수정
  • P2 : Comment
    • 반영해야 한다고 생각하는 부분
    • @멘션을 기반으로 토론 후 반영여부 결정하자
  • P3 : Approve
    • 넘어가도 좋은 의견들이 있다면 남기자
    • 이모지를 통해 읽었다는 표시를 남기자

또한, Conversation은 리뷰이가 resolve 할 수 없으며, 해결이 되었다면 댓글을 남긴 사람이 resolve를 해야 한다.

환경 재설정하기

본격적인 리팩토링에 앞서, 컨벤션 적용 및 필요없는 패키지 제거를 위하여 해당 부분을 반영하기 위한 PR을 제출했다. 이는 아직 리뷰중이다.

느낀 점

해당 코드를 작성할 때는 솔직히 코드가 괜찮게 짜여졌다고 생각했는데, 더 공부하고 다른 프로젝트를 하다가 돌아와보니 완전 엉망 진창이었다는 것을 느꼈다.

리팩토링은 늘 재밌다. 리팩토링을 통해 기존의 기능을 유지하면서도 다른 사람이 보기 좋은 코드를 만드는 과정 자체가 나에게 가장 보람을 주는 것 같다.

리팩토링과 별개로, 좋은 리팩토링을 위해서는 리팩토링을 해야겠다는 마음가짐 뿐 아니라, 나 혼자 리팩토링을 진행하는 것이 아니기 때문에 규칙과 리팩토링의 방향성, 그리고 올바른 리뷰 문화 3박자가 모두 맞아야 한다는 사실을 깨달았다. 이러한 룰을 정하는 과정 없이 리팩토링을 진행한다면, 결국 이전 코드와 다를 바가 없을 수도 있겠다는 생각을 했다.

다음에는 실제 리팩토링 과정을 통해 코드가 어떻게 변화했는지 알아보자.

profile
New Blog -> https://portfolio-mrbartrns.vercel.app

0개의 댓글