프론트엔드 리팩토링에 대하여 (Feat. 클린코드)

Lemon·2023년 2월 15일
3
post-thumbnail

회사에서 리팩토링을 시작하기에 앞서 어떤 방식으로 리팩토링하는게 좋을지 알아보는 과정을 가졌는데, 관련한 내용을 정리해 보려고 한다.

1. 각각의 비즈니스 로직 분리

  • 컴포넌트 렌더를 위한 코드와 비즈니스 로직을 분리하면 가독성이 좋아진다.
  • 비즈니스 로직 각각의 기능을 훅으로 추상화하면 단위 테스트 검증이 쉬워진다.
    리팩토링 이후 TDD를 작성할 예정이기 때문에 비즈니스 로직 분리는 반드시 필요한 과정이다.

2. 상태관리 라이브러리 사용하기

데이터 간의 의존성을 낮춰서 재사용성을 높이기 위해 상태 관리 라이브러리를 사용한다.
우리 회사에서는 Recoil을 사용하기로 했다.

3. 클린 코드 작성하기

제일 고민이 많았던 부분이다.
해당 내용은 아래의 영상을 참고해서 정리했다.

Ref.
토스ㅣSLASH 21 - 실무에서 바로 쓰는 Frontend Clean Code

실무에서의 클린 코드란 읽기 좋은 코드를 짜는 것이다.
읽기 좋은 코드를 짠다면 원하는 로직을 빠르게 찾을 수 있게 될 것이고, 그로 인해 유지 보수 시간이 단축될 것이다.
그렇다면 클린 코드를 작성하기 위해서 어떻게 해야 할까?

3-1. 응집도

선언적 프로그래밍

하나의 목적인 코드를 뭉쳐 둬야 한다.
우선 핵심 데이터와 세부 구현을 나눈다.
핵심 데이터는 밖에서 전달하고, 나머지는 뭉친다. 이렇게 하면 세부 구현을 읽지 않고도 어떤 내용인지 파악할 수 있다. 이렇게 작성하는 방식을 선언적 프로그래밍이라고 한다.
예를 들어 팝업창을 만든다고 가정했을 때, 핵심 데이터인 제목, 내용, 확인 버튼만 전달하고, 나머지 세부 구현은 뭉쳐서 숨겨두는 것이다.

(출처 : 토스 실무에서 바로 쓰는 Frontend Clean Code 캡쳐본)

위 사진의 내용을 React로 작성해 보자.

<Popup
	onSubmit={질문 전송}
	onSuccess={홈으로 이동}
/>

{질문 전송}, {홈으로이동}으로 '무엇'을 하는 함수인지 빠른 이해가 가능하고, '무엇'만 바꿔서 쉽게 재사용 가능하다.
onSubmit, onSuccess로 세부 구현은 내부에 뭉쳐둔다.

명령형 프로그래밍

'무엇'이 아닌 '어떻게' 해야 할지 하나하나 명령하는 것을 말한다.

코드로 예를 들자면 아래와 같다.

<Popup>
	<button onClick={async () => {
    	const res = await 회원가입();
      	if(res.success) {
        	프로필로이동();
        }
    }}>전송</button>
</Popup>

선언적 프로그래밍 vs 명령형 프로그래밍

무조건 선언적 프로그래밍이 좋은 것은 아니다. 두 방법 모두 의미가 있다. JSX 문법을 사용하면 HTML에서도 선언형 프로그램을 사용할 수 있다는 장점이 있지만, props로 '어떻게 해야 하는지'를 세부적으로 넘겨야 하는 경우에는 명령형 설계도 필요하다.

3-2. 단일 책임

1. 한 자기 일만 하는, 명확한 이름의 함수

함수가 한 가지 일을 하는 명확한 이름의 함수로 만들어야 한다.
만약 하나의 함수가 여러 가지 일을 한다면, 세부 구현을 모두 읽어야 함수의 역할을 알 수 있게 된다. 이렇게 되면 코드 추가 및 삭제도 오래 걸린다.

async function handle연결전문가질문제출(){
	await 연결전문가_질문전송(questionValue);
	alert(`${연결전문가.name}에게 질문이 등록되었어요.`);
}

async function handle새전문가질문제출(){
	await 질문전송(questionValue);
	alert("질문이 등록되었어요.");
}

async function handle약관동의팝업(){
	const 약관동의 = await 약관동의_받아오기();
	if(!약관동의){
		await 약관동의_팝업열기();	
	}
}

위와 같이 한 가지 일만 하는, 명확한 이름의 함수로 만들면 필요한 상황에 따라 따로따로 부를 수 있다.

2. 한 가지 일만 하는, 기능성 컴포넌트

<button
	onClick={async () => {
    	log('제출 버튼 클릭')
      	await openConfirm();
    }}
/>

위 코드는 버튼 클릭 함수에 로그 찍는 함수와 API 콜이 섞여 있다.
이것을 로그는 버튼을 감싼 컴포넌트에서 찍고, 버튼 클릭 함수에서는 API 콜만 신경 쓰도록 바꿔보자.

<LogClick message='제출 버튼 클릭'>
	<button onClick={openConfirm} />
</LogClick>

3-3. 추상화

도대체 추상화가 정확히 뭔지 이해가 잘 안됐었는데, 토스 영상을 보고 어느 정도 틀은 잡은 것 같다🤩

올바른 추상화란?

다른 개발자가 컴포넌트를 사용처에서 선언적으로 사용할 수 있어야 한다.
컴포넌트가 어떤 로직을 수행하는지 내용을 알 수 있어야 한다.
무엇을 하는지만 외부에서 보여지고, 어떻게 하는지는 컴포넌트 내부에서 처리한다.

프론트엔드 코드의 추상화란

컴포넌트 추상화 예를 들어보자.
팝업 코드를 제로부터 디테일하게 구현한 예제가 있다.

<div style={팝업스타일}>
  <button onClick={async ()=>{
    	const res = await 회원가입();
      	if(res.success){
        	프로필이동();
        }
    }}>전송</button>
</div>

위 예제를 중요 개념만 남기고 추상화한다면,

<Popup
	onSubmit={질문 전송}
	onSuccess={홈으로 이동}
/>

제출 액션과 성공 액션이라는 중요한 개념만 남기고 나머지를 추상화할 수 있다.

그렇다면 함수의 추상화는 어떻게 작성해 볼 수 있을까?
설계사 라벨을 얻는 코드를 세부 구현해 보자.

const planner = await fetchPlanner(plannerId);
const label = planner.new ? '새로운 상담사' : '연결중인 상담사';

전문가 정보를 받아 와서 응답 값에 따라 다른 라벨을 보여주는 코드이다.
위 코드를 중요 개념을 함수 이름에 담아 추상화해보자

const label = await getPlannerLabel(plannerId);

세부 구현을 getPlannerLabel이라는 함수명 안에 모두 추상화시킨 코드로 만들었다.

얼마나 추상화할 것인가?

<Button onClick={showConfirm}>
	전송
</Button>

{isShowConfirm && (
	<Confirm onClick={()=>{showMessage("성공")}}/>
)}

👇🏻Level 1.

<ConfirmButton
	onConfirm={()=>{showMessage("성공")}}>
	전송
</ConfirmButton>

👇🏻Level 2.

<ConfirmButton message="성공">
	전송
</ConfirmButton>

👇🏻Level 3.

<ConfirmButton />

상황에 따라 필요한 만큼만 추상화하면 된다.
그러나 유념하면 좋을 점이 있다.
추상화 수준이 섞여 있으면 코드 파악이 어렵다.

<Title>별점을 매겨주세요</Title> 👉🏻 높은 추상화

<div>
	{START.map(() => <Star />)} 👉🏻 낮은 추상화
</div>

<Reviews /> 👉🏻 높은 추상화

{rating !== 0 && ( 👉🏻 중간 추상화
	<>
		<Agreement />
		<Button rating={rating} />
	</>
)}

위 코드와 같이 추상화 수준이 섞여있으면 전체적인 코드가 어느 수준으로 구체적으로 기술된 지 파악할 수 없다.

<Title>별점을 매겨주세요</Title>
<Stars />
<Reciews />
<AgreementButton
		show={rating !== 0}
 />

상황에 따라서 낮은 추상화 단계로 정리해도 무방하다.

profile
개미는 뚠뚠..오늘도 뚠뚠🐜

0개의 댓글