본문 바로가기

FrontEnd

리액트 컴포넌트의 응집도를 관리하는 방법

반응형

리액트 컴포넌트의 응집도를 관리하는 방법을 알아봅니다.

같이 보면 좋은 글! : https://itchallenger.tistory.com/646

 

리액트 프로젝트의 결합도를 관리하는 방법

원문 : https://betterprogramming.pub/coupling-cohesion-552b022492b2 Organizing Modules in React Project — Low Coupling and High Cohesion Design robust applications betterprogramming.pub 서로 분리되..

itchallenger.tistory.com

해당 강연의 스크립트 정리 노트입니다 : https://youtu.be/aSAGOH2u2rs

TLDR

  • 응집도는 변화를 유발하는 원인이 얼마나 많은지를 나타내는 지표다
  • 응집도가 높다는 것은, 해당 모듈 / 클래스 / 함수 변경의 원인이 단 한가지 임을 의미한다.
  • 단일 책임 원칙, 횡단관심사

1년 전에 봤던 내용이지만 아직도 좋은 내용이라 생각하여 복습 겸 정리노트를 작성합니다.

여기에서 말하는 응집도라는 개념은 메서드 내 로직(라인 한 줄 / 호출대상 메서드 / 함수)들이 단 하나의 일만 하는지를 말하는 것 같습니다.

  • 즉, 라인 한 줄/ 한 줄 간 마이크로한 단위의 의존성을 의미합니다.
    • 함수 내 함수 호출이 아닌 라인 간 의존성 입니다.
      • 이전 라인 실행 결과가 다음 라인에서 사용되면 응집도가 낮아집니다. (순차적 결합도)
      • 이전 라인과 다음 라인이 메서드 내에서 반드시 같이 실행되어야 한다면 (ex 커넥션 및 클린업).응집도가 낮아집니다. (시간적 결합도)
  • 여러 메서드/ 클래스 / 모듈이 같은 필드를 공유하며 수정하고 조회하면 응집도가 낮아집니다(통신적 결합도)
  • 플래그를 이용해 로직을 분기하면 응집도가 낮아집니다. (논리적 결합도)
  • 메서드 간의 호출은 응집도를 해치지 않습니다.
  • 메서드/ 함수 내부 로직(각 라인)들 간의 의존성은 응집도를 해칩니다.
  • 하나의 기능만을 위해 존재하고, 이 기능만을 위한 데이터를 모두 활용하고 있으면 기능적 응집도가 높은것입니다.

좀 더 자세히 살펴볼까요?

메서드 / 함수

  • 메서드 / 함수가 클래스의 모든 필드를 사용하지 않으면 응집도가 낮음
    • 순수 함수는 모든 파라미터를 활용하는지만 보면 됩니다.
    • ISP 위반
  • 메서드 / 함수가 모든 파라미터를 활용하지 않으면 응집도가 낮음
    • ISP 위반
  • 메서드 / 함수 내부 루틴(각 라인)이 동시(병렬적으로)에 실행될 수 있어야함.
  • 메서드 / 함수 내부에서 다른 메서드 / 함수를 호출하는 것은 전혀 상관없습니다.
  • 탬플릿 메서드 패턴
    • 각 메서드 / 함수 에 절차,시간적인 의존성이 존재함
    • 각 절차 / 루틴을 메서드 / 함수화해서 순서대로 동기적으로 호출함.
      • 내부에 DI를 통한 로직 주입
        • DI는 구체 클래스/함수와의 커플링을 줄이기 위한 수단임 (by interface)
    • 탬플릿 메서드 내 각 루틴(로직 혹은 메서드 호출)은 병렬 / 동시적으로 실행 가능해야 한다.

데이터

  • 데이터의 생명주기를 자신이 책임진다.
  • pub / sub 패턴을 통해 업데이트 갱신의 책임을 자기가 진다.

컴포넌트(리액트)

  • 사용하지 않는 prop을 파라미터로 받지 않음
  • prop 데이터를 이용한 렌더링 -> prop 모두 사용
  • 콜백을 이용한 인터랙션
  • 데이터 스토어가 자신의 데이터의 생명주기를 펍섭 콜백을 통해 변화를 허용한다면
  • 컴포넌트는 자신의 렌더링 생명주기를 이벤트 리스너 콜백을 변화를 허용함
  • 원리는 동일하다!

저는 소프트웨어에 정답은 없지만 해법은 존재한다고 생각합니다.

응집도란 모듈이 책임을 다하기 위해서 필요한 데이터와 메소드들이 얼마나 서로 밀접하게 연관되어 있는지를 나타냅니다.

응집도를 세부적으로 나눠보면 기능적, 순차적, 통신적, 시간적, 절차적, 논리적, 그리고 우연적 정도로 나눌 수 있습니다.

 
순서대로 앞에 있는 응집도를 가지는 것이
뒤에 있는 응집도를 가지는 것보다
일반적으로 더 좋다고 말할 순 있습니다.
 

기능적 응집도

 
기능적 응집도는 모듈 안에 모든 요소들이 모듈의 기능을 위해서 함께 작동하는 것을 말합니다.
모듈이 하나의 기능만 하고 모듈 요소들은 이 하나의 기능을 위해서 필수적인 부분일 때 높은 기능적 응집도를 가집니다.
우리가 흔히 알고 있는 단일책임원칙과도 상당히 연관이 많습니다.
 
기능적 응집도
만약 모조리 작은 단위로 기능을 하고 있다면
기능적 응집도를 가지고 있는지 판단하는 것은 쉬울 수 있지만
그렇지 않다면 응집도를 파악하는 것조차 어려울 수 있습니다.
따라서 모듈이 하나의 책임만 가질 수 있도록 작게 나누는 것이 좋습니다.
 
기능적 응집도를 갖게 하기 : 모듈을 작게 나눈다.

순차적 응집도

순차적 응집도는 루틴이 특정한 순서대로 수행되어야 하고,
단계마다 정보를 공유하며 동시에 수행될 때 완전한 기능을 제공하지 못하는 연산을 포함할 때 존재하게 되는데요,
 
만약에 왼쪽의 코드처럼 직원의 입사일과 근속연수를 이용해서
퇴직일을 계산하고 난 다음에 퇴직금을 계산 한다면,
이 두 개로 지금 순차적 응집도를 가지게 됩니다.
 
 
이러한 응집도를 기능적 응집도를 가지도록 만드려면, 즉, 하나의 일만 하도록 하려면
  • 오른쪽에 코드와 같이 퇴직일을 계산하는 로직과
  • 퇴직금을 계산하는 로직을 각각 분리해야 합니다.
순차적 응집도 > 로직 분리로 해결

 

그리고 퇴직금을 계산하는 로직은 퇴직일을 계산하는 로직을 실행할 수 있고
그러면 두 가지 로직은 모두 기능적 응집도를 가지게 됩니다.
순차적 응집도를 기능적 응집도를 갖게 하기 : 모듈을 작게 나눈다.

 

통신적 응집도

통신적 응집도는 모듈이 같은 데이터를 사용하지만 서로 아무런 관련성이 없을 때 발생합니다.
각 모델이 하는 일이 다르지만 같은 데이터를 사용하기 때문에 응집도가 발생합니다.
같은 데이터를 사용하지만 하는 일이 관련이 없는 모듈
예를 들어서 어떤 모듈이 전달되는 데이터를 가지고
무엇을 출력하고 나서 그 데이터를 초기화하는 기능을 하고 있다면
이 기능들은 같은 데이터를 사용한다는 것을 제외하면
아무런 관계가 없습니다.
 
더 나은 응집도를 위해서는
데이터를 초기화하는 기능을
출력하고 사용하는 곳이 아니라
데이터를 생성한 곳에서 해야 합니다.

 

통신적 응집도 해결 : 데이터 초기화에 대한 책임을 데이터를 생성 및 제거하는 클래스, 모듈에 부여
좋은 예시는 리덕스 스토어
 
 

[번역] 초보 프론트엔드 개발자들을 위한 Pub-Sub(Publish-Subscribe) 패턴을 알아보기

비동기 자바스크립트 코드를 덜 괴롭게 이해하는 방법

www.rinae.dev

교훈 :

  • RxJS, 리덕스 스토어 등은 발행 / 구독 패턴을 사용한다.
  • 해당 패턴의 핵심은 다름아닌 통신적 결합도를 없애기 위함이다.

시간적 응집도

시간적 응집도는 여러 연산이 동시에 수행되어야 해서 하나의 로직으로 결합될 때, 발생하는 응집도입니다.

가장 단순한 예로, 어떠한 모듈을 초기화하고
무언가를 실행한 다음에 정리하는 로직이 서로 연관되어 있을 때
이 응집도가 발생합니다.
하나의 메서드에 위 개별 메서드가 다 들어가 있으면 시간적 응집도 발생
해결 방법 :
위처럼 시간별로 수행되는 메서드를 나눔
탬플릿 메서드 패턴 적용 (콜백 인자로?)

 

절차적 응집도

절차적 응집도는 모듈이 프로시저에만 집중하여 정해진 순서대로 처리하도록 할 때 발생하는 응집도입니다.

절차, 시간적 응집도와 달리,메서드 내부 로직 간 순서 의존성, 시점 의존성을 의미하는것이 아니라,
메서드 자체가 어떤 로직, 책임 기준이 아닌(데이터 조작), 업무 처리 절차 그 자체(표 구매, 입장, 관람 퇴장)에 초점을 두어 구현되었을 경우를 의미하는것 같습니다.
절차적 응집도
 
이 응집도는 시간적 응집도와 같이 실행 순간에 따라 그룹화될 뿐만 아니라
특정 순서로 실행되어야 함으로 응집도가 높아지는데,
이는 프로시저와 프로시저 사이의 강한 결합을 만들기 때문에 주의해야 합니다.
꼭 순서대로 실행될 필요가 없다면 분리하여,
각 기능이 기능적 응집도만을 가지도록 하는 것이 좋을 수 있습니다.
혹은 반대로 서로 분리가 되어 있어
순서대로 의존하도록 만들 수도 있는데
이것에 정답은 없습니다.
응집도를 분리할 경우 한 번에 이해해야 하는 정보의 양이 적어지기 때문에
각각을 이해하기 쉬워지겠지만
너무 나누어진다면 각 모델들이 합쳐서 무슨 일을 하는지
파악하기 어려울 수 있습니다.
 
해결 방법 : 업무에서 데이터 변환 중심으로 로직 구현 단위를 더 잘게 쪼갠다.

논리적 응집도

논리적 응집도는 여러 가지 기능을
하나의 루틴에서 수행할 때
루틴에 전달되는 조건에 따라
수행하는 기능이 다른 경우에 발생합니다.
 
예를 들어서 이름과 핸드폰 번호를 받는 컴포넌트가 있고, 값이 변경되었을 때 처리하는 함수가 있다고 하겠습니다.
그 각각의 함수를 동일한 함수로 처리를 하고 있는데요,
이때 이름이면 데이터를 단순히 변경하고 있지만
핸드폰 번호일 경우에는 '-'를 지우고 변경하고 있습니다.
 
 
함수 안에서 넘어 오는 플래그를 이용해서 다른 동작을 하도록 해서는 안 됩니다.
왜냐하면 플래그를 이용해서 다른 동작을 하는 것이 곧 두 가지 이상의 일을 하고 있다는 것이고,
플래그에 따라 다르게 동작한다는 것을 예측하기가 매우 어렵기 때문입니다.
따라서 오른쪽에 handleChangeName()과 handleChangePhone()처럼
독립적인 일을 하는 함수로 분리해야 합니다.
그렇게 되면 각 함수는 하나의 일만 하기 때문에
기능적 응집도를 가집니다.
해결 방법 :
함수 내에서 플래그를 통한 로직 선택을 하지 않도록 한다.
하나의 컴포넌트에 다른 로직을 바인딩해야 한다면
조건 판단 함수를 따로 만들고, 해당 로직 구현 함수를 각각 작성하는것도 나쁘지 않은 방법임 (ex reducer)
 

우연적 응집도

 
우연적 응집도는 모듈의 요소들이 특별한 연관관계를 맺지 않을 때
발생하는 응집도입니다.
모듈의 요소들이 완전히 무작위로 그룹화
된다는 것을 의미합니다.
이러한 모듈들은 의도적으로 만들려고 만든 것이 아니라
그냥 놓을 곳이 없어서 놓을 때 발생합니다.
대표적으로 유틸 모듈이라고 부르는 모듈이 있습니다.
 
그저 어디에 넣을 줄 몰라서
마구잡이로 기능들을 추가하고
요소들을 추가할 경우 우연적 응집도를 가지게 되므로
반드시 피해야 하는 응집도입니다.
이러한 응집도들을 알아봤는데요,
우리들의 컴포넌트에서는 어떤 의미를 가지는 지 한번 살펴보겠습니다.

컴포넌트 관점에서 알아보기

여기에 Pagination 컴포넌트가 있습니다.

이 컴포넌트는 페이지, 페이지 당 개수,
그리고 전체 개수를 props로 받고 있습니다.
페이지를 클릭하면 onChangePage 이벤트가 발생합니다.
페이지 당 개수를 변경하면
onChangeSize 이벤트가 발생합니다.
따라서 각각 메소드의 관계를 그림을 그리면 아래처럼 될 거예요.
 
 
 
다음과 같이 두 개의 컴포넌트를 분리할 수 있습니다.
페이지 당 개수를 변경할 수 있는 컴포넌트와
Pagination 정보를 보여주는 두 개 컴포넌트로 분리할 수 있죠.
각 컴포넌트가 단 하나의 일만 하고 있고
내부 데이터를 모두 활용하고 있어
데이터와 메소드가 끈끈하게 연결되어 있으니
높은 응집도를 가진 컴포넌트가 될 것입니다.
 
그런데 이 컴포넌트에서 드러나진 않지만 사실은 페이지 당 개수를 변경했을 때
현재 페이지는 1로 초기화되어야 합니다.
(즉, page 상태를 변경하는 방법을 알아야 함.)
 
따라서 onChangeSize 이벤트가 page에도 의존하기 때문에 PagingSize라는 컴포넌트는 외부의 page에 의존하게 됩니다.
따라서 PagingSize는 응집도가 낮은 컴포넌트가 되는 것이고
따라서 페이지당 개수와 현재 페이지, 아이템들의 전체 개수는 더 작게 나눌 수 없는 데이터라고 바라본다면 다음과 같이 만들 수 있습니다.
 
이제 Pagination 컴포넌트는 페이징이라는 데이터를 받고
페이징이 변경되었을 때 onChange 이벤트가 발생합니다.
Page, Size, Total은 더 이상 작게 나눌 수 없는 데이터입니다.
이 컴포넌트가 페이징을 변경하는 일을 하고 있는 컴포넌트라고 한다면
데이터와 메소드가 서로 밀접하게 연관되어 있으니
높은 응집도를 가진 컴포넌트라고 볼 수 있겠죠.
그런데 컴포넌트의 행위가 데이터와 연관관계가 있다고 해서
응집도가 높은 컴포넌트라고 이야기할 수 있을까요?
자 이제, 새로 고침을 하더라도
이전에 선택 했던 페이지를 기억하기 위해서
Pagination 정보를 수정할 때마다
URL의 쿼리를 수정하도록 하는 기능을 추가하려고 합니다.
페이지의 정보를 수정할 때 onChange 이벤트를 발생하면서
동시에 브라우저의 location API를 사용해서 쿼리를 수정하는 거죠.
분명히 모든 메소드들이 데이터와 연관관계가 있어,
응집도가 높다고 판단할 수 있습니다.
하지만 저희가 중요한 것을 하나 빠뜨린 것이 있습니다.
바로 컴포넌트의 목적이 무엇이냐는 거죠.
이 컴포넌트는 두 가지 목적을 가지고 있습니다.
  • 페이지네션을 수정하는 것,
  • 그리고 URL의 쿼리를 수정하는 것,
메소드와 데이터의 연관관계만 보면 응집도가 높다고 판단할 수 있지만
객체의 목적이 두 가지이기 때문에
이 컴포넌트를 수정해야 하는 이유가 두 가지가 되었고
결국 기능적 응집도가 하나 낮아지게 되었습니다.
앞에서 살펴본 것처럼
높은 응집도가 있는 컴포넌트를 만들려면
컴포넌트의 목적을 명확히 하고 이 목적을 위해서
데이터와 메소드가 똘똘 뭉치게 해야 합니다.

높은 응집도

그런데 우리는 왜 꼭 높은 응집도를 만들어야 할까요?
  • 왜냐하면 높은 응집도인 컴포넌트는 이해하기 쉽고
  • 의도를 파악하기 쉽게 하고
  • 그리고 테스트하기 쉽기 때문입니다.

이해하기 쉽다

왜 응집도 높은 모듈이 이해하기 쉽게 만드는지

우리가 일할 때 대화 속에서 한번 살펴볼게요.
프론트 개발자가 API를 연동하는 개발을 하고 있었는데 에러가 났습니다.
"왜 에러가 났지?
서버 개발자에게 이제 문의를 하는 거죠.
 
프론트 개발자: 개발자 님, API 에러가 납니다.
서버 개발자: "어떤 API죠?' 프론트 개발자: "주문 조회 API입니다." 서버 개발자: "어떻게 안되나요?"
프론트 개발자: "에러가 응답이 와요." 서버 개발자: "응답 결과를 보여줄 수 있나요?"
프론트 개발자: 주문을 찾을 수 없습니다.
서버 개발자: 어떻게 요청하셨나요? 프론트 개발자: 이렇게 요청했습니다. (/orders/1004)
서버 개발자: 확인해 보니 결제가 실패한 주문이네요.
서버 개발자: 실패한 주문은 조회할 수 없습니다.
자 이 대화를 보면 어떤 생각이 드시나요?
좋은 커뮤니케이션이라고 볼 수 있을까요?
프론트 개발자는 왜 이 에러가 발생했는지 알고 싶고
서버 개발자는 그 해답을 주고 싶어 합니다.
그런데 해답을 주기 위해 필요한 정보들이 흩어져 있다 보니
이해하기도 어렵고 필요 없는 이야기들이 오고 가고 있습니다.
판단할 수 있는 모든 정보
 
만약에 이 대화를 이렇게 해 보면 어떨까요?
프론트 개발자는 서버 개발자가 에러 확인할 수 있는
모든 정보를 같이 제공했습니다.
어떤 API에서 문제가 있는지, 요청은 어떤 값을 했는지
그에 따라 응답은 어떻게 왔는지를 같이 제공했습니다.
그럼 서버 개발자는 에러를 판단할 수 있는 모든 정보가 있기 때문에
더 이상 되물을 필요 없이
바로 확인하고 응답을 줄 수 있습니다.
이 대화는 여러 파악이 필요한 데이터가
요청과 긴밀하게 연관되어 있어 인지도가 높은 질문이었습니다.
 
 
이전 커뮤니케이션에서는 API 요청값,
응답값을 따로 따로 물어봤습니다.
그러다보니 커뮤니케이션 개수도 많아졌고
필요한 데이터를 늦게 받았습니다.
서버 에러를 파악하는데 있어서 API 요청값, 응답값은
서로 복잡하게 연관되어 사용되기 때문에 높은 응집도를
가지도록 설계해야 합니다.
그래서 오른쪽과 같이 설계한다면
처음부터 프론트 개발자에게 에러를 파악할 때는 API와 요청값,
그리고 응답값이 모두 필요하니
한 번에 주세요라고 얘기할 수 있겠죠.
HTTP 클라이언트를 바로 실행할 수 있도록 전달합니다.
저는 더 나아가서
서버 개발자 분들이 인텔리제이를 많이 사용하기 때문에
HTTP 클라이언트를 바로 실행할 수 있도록 전달합니다.
혹은 쿼리를 바로 실행 해 볼 수 있도록 전달할 수도 있겠죠.
정보를 제공할 때 데이터를 사용한 주체가 더 쉽게 사용할 수 있도록
전달하면 더 좋을 거예요.

의도를 파악하기 쉽다

두 번째, 높은 응집도 있는 모듈을 만들어야 하는 이유는
높은 응집도를 가진 모듈은
의도로 파악하기 쉽게 만들기 때문입니다.
이 pull request는 주문을 찾을 수 없는 경우에
팝업을 보여 주기 위해서 만든 pull request입니다.
커밋을 살펴보면, 위의 세 개는 pull request 목적에 맞는 커밋들입니다.
주문을 찾을 수 없고
그리고 그 에러를 나타내는 에러를 정의했고
해당 에러가 발생했을 때 에러를 던지도록 했으며
그래서 화면에서 팝업을 보여 주도록 했습니다.
이 커밋들이 모두 pull request의 목적을 위해서
존재하고 있기 때문에 이해하기 쉽습니다.
커밋의 낮은 응집도
그런데 아래 두 개 커밋이 더 있는데,
이 커밋들은 pull request의 목적과는 상관이 없어 보입니다.
ESLint를 수정하고 있고
또, 이 페이지와 상관 없는 메인 페이지의 무언가를 변경하고 있습니다.
그래서 오히려 이 커밋 때문에
이 pull request 목적을 이해하는 데 도움이 안 되고 있습니다.
그리고 이제 코드 리뷰를 받을 텐데
코드리 뷰를 통해서 수정해야 할 것들이 생길 수 있습니다.
아까 만들었던 에러를 수정할 수도 있고요,
팝업 메시지도 수정할 수 있겠죠?
그런데 ESLint들을 수정하더라도 이 pull request에서 수정이 일어나야 됩니다.
메인 페이지의 무언가를 바꾸더라도 이 pull request를 고쳐야 되는 거죠.
즉, 이 pull request를 변경할 이유가 세 가지나 되는 거죠.
목적이 세 가지가 되니 기능적 응집도가 낮아졌습니다.
 
그래서 이 pull request가 하나의 기능만 할 수 있도록 세 개로 분리했습니다.
커밋의 높은 응집도
이제, 각 pull request는 변경해야 하는 이유는 단 하나로,
응집도가 높은 pull request가 되었습니다.
아무리 커밋이 많더라도
단 하나의 목적을 위해서 존재한다면 일관성이 있고
작업의 의도를 드러내고 이해하는 데 도움이 될 것입니다.
반면 단 두 개만 있더라도
서로 다른 목표를 위해 존재한다면,
작업에 대한 의도를 이해하기 어려워질 것입니다.
따라서 높은 응집도 있는 모듈은 그 모듈의 의도를 파악하기 쉽게 만들어 줍니다.

테스트하기 쉽다 

TLDR : 컴포넌트 단위 테스트에서는 렌더링과 인터렉션만 다뤄야 한다. 다른건 훅으로 빼낸다.

마지막으로 응집도가 높은 분들은 테스트하기 쉽게 만들어주는데요,
아까 만들었던 Pagination 컴포넌트 테스트를 작성해 보면 다음과 같습니다.
페이지를 클릭하면 페이지가 변경이 되고
페이지 당 개수를 변경하면 페이지가 1로 초기화되면서 페이지 당 개수가 변경됩니다.
그리고 페이지를 클릭하면 URL의 쿼리가 변경이 되고
페이지 당 개수를 변경해도 URL의 쿼리가 변경되는 것을 테스트하고 있습니다.
 
위에 두 개 테스트는 변경 했을 때 화면에 잘 보이는 지를 테스트 하고 있지만
아래의 테스트는 URL의 쿼리가 잘 변경됐는 지를 테스트하고 있습니다.
 
만약에 페이지를 이동하기, 처음 페이지로 이동하기를 만들 경우
위에 테스트를 수정하거나 추가해야 하고
또 Pagination 컴포넌트도 수정해야 합니다.
 
또 만약, URL의 쿼리 이름이라던가 전체 개수를 쿼리에 추가하고 싶다면 아래 테스트를 수정해야 하고, 이 Pagination 컴포넌트도 수정해야 합니다.
 
즉 페이징이라는 데이터를 변경하는 행위는 하나와 URL의 쿼리를 변경하는 행위, 2가지 행위를 하고 있기 때문에,
기능적 응집도가 낮아졌습니다.
기능적 응집도를 높이려면, 컴포넌트가 한가지 일만 해야 합니다.
그리고 URL의 쿼리를 변경하는 것을 보면, 페이징의 정보를 변경하고 나서 URL의 쿼리를 변경하고 있습니다.
변경할 때 사용하는 데이터는 클릭한 데이터를 사용하고 있으므로
이 로직은 서로 같은 데이터를 공유하고 있지만
서로 상관없는 일을 하고 있습니다.
그래서 단순히 절차적으로 응집이 되어 있습니다.
그리고 같은 데이터를 공유하고 있다 보니
통신적 응집도가 가진 상태입니다.
심지어 두 가지 일을 하고 있으니
기능적 응집도는 떨어지는 상황입니다.
필요 없는 절차적 응집도와 통신적 응집도를 제거하고
기능적으로 응집이 되도록 분리해야 하는 상황입니다.
 
페이징의 데이터를 변경하는 것과 URL의 쿼리를 갱신하는 것은 서로 상관이 없으므로
순서에 상관 없이 동작할 수 있도록 만들면 절차적 응집도가 없어져서 더 높은 응집도 있는 컴포넌트가 될 것입니다.
대신에 같은 데이터에 의존하고 있으니 통신적 응집도는 아직 존재하는 상황이고,
컴포넌트가 두 가지 일을 하고 있으니 기능적 응집도는 낮은 상태입니다.
컴포넌트가 하나의 일만 하도록
URL의 쿼리를 갱신하는 것을 다른 곳으로 옮겨 보겠습니다.
URL 쿼리를 갱신하는 부분을 useEffect로 추출
Pagination 컴포넌트에서 URL 쿼리를 갱신하는 코드를 바깥으로 분리했습니다.
이제 Pagination의 페이징을 변경하는 일과 보여주는 일만 하고 있습니다.
따라서 기능적 응집도가 높아졌습니다.
Pagination의 테스트도 오른쪽처럼 더 간단해졌습니다.
 
테스트의 개수가 줄었다고 무조건 하기 쉽냐라고하면 꼭 그런 것은 아닙니다.
하지만 테스트가 점점 많아지고
테스트하는 대상이 점점 많아질수록 무언가가 잘못된 것이 있나 의심해 보는 것은 좋다고 생각합니다.
 
그런데 응집도가 낮아지면 대체 무슨 일이 일어나길래 이렇게 응집도 높은 컴포넌트를 설계하는 걸까요?
만약에 아까처럼 기능을 분리하지 않은 상황에서
상품의 카테고리를 선택하는 기능도 생겼다고 가정하겠습니다.
이 선택된 결과도 마찬가지로 URL의 쿼리를 갱신해야 합니다.
그래서 카테고리를 수정할 때마다
기존 커리 뒤에 카테고리를 추가하도록 기능을 만들었고
잘 동작하는 것을 확인하고 배포를 완료했습니다.
그런데 갑자기 무언가를 만지면 카테고리가 사라져버립니다.
바로 Pagination 페이지를 변경하면
URL의 쿼리를 수정하도록 되어 있기 때문이죠.
물론 아까처럼 모든 테스트 코드가 작성되어 있다면
이 컴포넌트에서 URL의 쿼리를 변경 하고 있다는 사실을 알아 냈을 수도 있겠지만
내가 무언가를 수정할 때마다
모든 테스트 코드를 들여다 볼 수도 없고
모든 코드들을 찾아보기란 정말 어려운 일이죠.
그리고 이 컴포넌트에서 이 URL의 쿼리를 갱신하고 있다는 것을 예측하는 것은
정말 더 어려운 일입니다.
왜냐하면 이 컴포넌트가 두 가지 일을 하고 있기 때문이죠.
 

테스트를 작성하기

 
대부분의 문제는 내가 만든 코드에
어떤 변경 사항이 일어나거나
혹은 내가 만든 것을 다른 사람이 사용할 때
그 때 발생합니다.
 
아까처럼 나도 모르게 기능을 수정하는 것이 있을 수도 있고요.
또, 다른 사람이 내 코드를 볼 때는 처음 보는 코드이니
이해해야 하고, 사용법을 이해해야 합니다.
응집도가 낮은 코드들은 이해하기도 어려울 뿐더러
의도로 파악하기 어려워서 사용하기 어렵게 만듭니다.
 
이러한 피드백을 받으려면 변경 사항 일어나거나
다른 사람이 내 코드를 사용해야 비로소 피드백을 얻을 수 있는데
무작정 기다릴 수는 없는 상황입니다.
 
이때 테스트를 작성하면 당장에
내가 작성한 코드의 사용자가 되는 것이고
그리고 테스트 코드는
내 코드를 사용하는 일종의 설명서 역할을 하기 때문에
내 코드를 이해하기 쉽게 만들어 줍니다.
 
그런데 테스트가 아주 많다면
아무리 테스트가 있더라도 이해하기 어려워지겠죠?
만약에 내가 전자레인지를 샀는데,
설명서가 막 100페이지가 넘어간다고 생각해 봅시다. 생각만 해도 힘들겠죠?
 
그래서 테스트를 짜는 것이 어렵다면
해당 코드를 사용하기가 어려운 것은 아닌지
그런 신호로 받아들여야 합니다.
 
코드를 작성하기 전에 먼저 테스트를 작성 한다면
내가 코드를 작성 하기 전부터 어떻게 사용할 지 고려할 수 있기 때문에
처음부터 사용하기 좋은 코드를 설계할 수 있습니다.

먼저 테스트를 작성하기

테스트를 먼저 작성한다면
내 코드를 올바르게 동작하는지 검증해 주는 단순한 도구가 아니라
인지도 있는 설계,
사용하기 좋은 코드를 만드는데
도움이 되는 도구로 활용할 수 있습니다.
 
높은 응집도를 가진 모듈은 이해하기 쉽고, 의도를 파악하기 쉽고,
테스트하기 쉽게 만들어 준다고 했습니다.
그렇다고 해서 무조건 높은 응집도를 가진 것이 좋은 것이 아니라
그 목적에 맞게 필요한 만큼만 응집되어 있어야 합니다.

응집도는 컴포넌트를 나누는 기준이 됩니다.

지금까지 응집도과 무엇이고 왜 높은 응집도가 있는 컴포넌트를
만들어야 하는 지 알아 보았는데요,
응집도가 무엇인지 이해하는 것만큼
낮은 응집도를 발견하는 것 또한 굉장히 중요합니다.
높은 응집도를 가지는 조건을 내가 모두 안다고 하더라도
낮은 응집도를 발견하여
대응하지 못한다면 쓸모가 없겠죠.
마치 리팩터링하는 방법을 모두가 알고 있지만
언제 리팩터링을 해야 할지 모르는 것과 같습니다.
테스트를 통해서 응집도 있는 컴포넌트로 나누는 방법을 알아보겠습니다.
 
이전에 테스트에서 페이징을 변경하는 것에 대해서만 테스트를 작성했는데요,
이것만으로는 이 컴포넌트가 올바르게 동작하는지
모두 테스트하고 있지 않습니다.
핵심 기능에 대해서는 모두 테스트를 작성했지만
그 외에도 검증할 부분이 많이 남아 있었습니다.
선택할 수 있는 페이지, 선택할 수 있는 페이지 당 개수는 어떤 것들이 있는지
이전 버튼과 다음 버튼은 언제 활성화되어야 하고
언제 비활성화 되어야 하는지,
선택된 페이지와 선택되지 않은 페이지들은 어떻게 되는지
테스트 코드가 존재하지 않았습니다.
테스트 코드를 작성하다 보니까
Pagination 테스트의 테스트가 점점 늘어납니다.
이렇게 테스트가 늘어갈 때
컴포넌트를 더 작게 나누어야 하지 않냐라는 신호로 받아들여야 합니다.
PagingSizeSelect
Pagination 컴포넌트 내부 코드에서 페이지 당 개수를 PagingSizeSelect 컴포넌트로 분리하고
테스트를 작성해 보는 오른쪽과 코드와 같이 작성할 수 있습니다.
이 컴포넌트는 페이지 당 개수를 보여 주고,
선택할 수 있는 페이지 당 개수 목록을 가지고 있어서 선택할 수 있습니다.
만약 페이지 당 개수를 변경하면
onChange 핸들러 함수를 호출하게 되고, 선택한 페이지 당 개수를 전달합니다.
따라서 이 함수는 하나의 일만 하고 있고
응집도 높은 컴포넌트라고 할 수 있습니다.
이 컴포넌트를 사용하는 사용자 컴포넌트인 Pagination 컴포넌트에서는
단순히 이 컴포넌트를 사용하는 입장이고
나머지 모두 이 PagingSizeSelect라는 컴포넌트가 응집도 있게 기능을 처리하고 있습니다.
PreviousButton
이전 버튼을 클릭했을 경우 onClick을 실행하게 되는데
첫 번째 페이지라면 클릭할 수 없기 때문에
클릭 하더라도 onClick 핸들러 함수를 호출하지 않습니다.
이러한 기능들은 Pagination 컴포넌트에서는 관심이 없기 때문에
하위 컴포넌트로 분리를 했고요, 이 기능들에 대해서
PreviousButton 컴포넌트가 책임지고 있습니다.
그래서 PreviousButton 컴포넌트가 활성화되어 있을 때
클릭하면 onClick 핸들러 함수를 호출한다는 것,
비활성화되어 있을 때 클릭하면 onClick 핸들러 함수를 호출하지 않는다는 것,
이 기능만을 위해 존재하고, 이 기능만을 위한 데이터를 모두 활용하고 있어
기능적 응집도가 높다는 것을 확인할 수 있습니다.
PageButton
PageButton은 페이지를 받아서 보여주고, 클릭했을 때
onClick 핸들러 함수를 호출하고 있습니다.
만약 페이지가 현재 선택되어 있다면 선택됨을 표시합니다.
페이지가 선택되었을때 어떻게 표시할 지는 이 컴포넌트가 결정하는 것이고
결정하는 데 필요한 데이터를 모두 가지고 있습니다.
그리고 이 데이터를 모두 이용하여 선택됨을 표시하고 있어서
응집도 높은 컴포넌트라고 할 수 있습니다.
만약 선택됨이라고 표시하지 않고
다른 스타일이나 그것의 색깔같은 것으로 표시하기로 변경이 되었다면
Pagination 컴포넌트가 아닌, 이 PageButton 컴포넌트만 변경할 것이고
이러한 방법은 Pagination에는 영향이 없을 것입니다.
Pagination의 관심은 오직 페이징 데이터를 변경하고
변경했을 때만 보여 주는 일만 하고 있기 때문입니다.
 

마치며

응집도는 모듈의 목적을 위해서
요소들이 밀접하게 연관되어 있는 것을 말합니다.
높은 응집도 있는 모듈은 이해하기 쉽고
의도를 파악하기 쉽게 하고
그리고 테스트하기 쉽게 만들어 줍니다.
 
테스트를 작성하여 응집도가 높은지
낮은지 파악해 볼 수 있으며
응집도에 따라서 컴포넌트를 분리할 수 있습니다.
 
그렇게 컴포넌트를 만들 때
응집도가 있는지 어떤 응집도가 있는지, 또 낮은 응집도 때문에 변경이 어려운지를 판단해서
더 좋은 컴포넌트를 만들어 보면 좋을 것 같습니다.
 
반응형