회사에서 리팩토링을 시작하기에 앞서 어떤 방식으로 리팩토링하는게 좋을지 알아보는 과정을 가졌는데, 관련한 내용을 정리해 보려고 한다.
데이터 간의 의존성을 낮춰서 재사용성을 높이기 위해 상태 관리 라이브러리를 사용한다.
우리 회사에서는 Recoil을 사용하기로 했다.
제일 고민이 많았던 부분이다.
해당 내용은 아래의 영상을 참고해서 정리했다.
실무에서의 클린 코드란 읽기 좋은 코드를 짜는 것이다.
읽기 좋은 코드를 짠다면 원하는 로직을 빠르게 찾을 수 있게 될 것이고, 그로 인해 유지 보수 시간이 단축될 것이다.
그렇다면 클린 코드를 작성하기 위해서 어떻게 해야 할까?
하나의 목적인 코드를 뭉쳐 둬야 한다.
우선 핵심 데이터와 세부 구현을 나눈다.
핵심 데이터는 밖에서 전달하고, 나머지는 뭉친다. 이렇게 하면 세부 구현을 읽지 않고도 어떤 내용인지 파악할 수 있다. 이렇게 작성하는 방식을 선언적 프로그래밍이라고 한다.
예를 들어 팝업창을 만든다고 가정했을 때, 핵심 데이터인 제목, 내용, 확인 버튼만 전달하고, 나머지 세부 구현은 뭉쳐서 숨겨두는 것이다.
(출처 : 토스 실무에서 바로 쓰는 Frontend Clean Code 캡쳐본)
위 사진의 내용을 React로 작성해 보자.
<Popup
onSubmit={질문 전송}
onSuccess={홈으로 이동}
/>
{질문 전송}
, {홈으로이동}
으로 '무엇'을 하는 함수인지 빠른 이해가 가능하고, '무엇'만 바꿔서 쉽게 재사용 가능하다.
onSubmit
, onSuccess
로 세부 구현은 내부에 뭉쳐둔다.
'무엇'이 아닌 '어떻게' 해야 할지 하나하나 명령하는 것을 말한다.
코드로 예를 들자면 아래와 같다.
<Popup>
<button onClick={async () => {
const res = await 회원가입();
if(res.success) {
프로필로이동();
}
}}>전송</button>
</Popup>
무조건 선언적 프로그래밍이 좋은 것은 아니다. 두 방법 모두 의미가 있다. JSX 문법을 사용하면 HTML에서도 선언형 프로그램을 사용할 수 있다는 장점이 있지만, props로 '어떻게 해야 하는지'를 세부적으로 넘겨야 하는 경우에는 명령형 설계도 필요하다.
함수가 한 가지 일을 하는 명확한 이름의 함수로 만들어야 한다.
만약 하나의 함수가 여러 가지 일을 한다면, 세부 구현을 모두 읽어야 함수의 역할을 알 수 있게 된다. 이렇게 되면 코드 추가 및 삭제도 오래 걸린다.
async function handle연결전문가질문제출(){
await 연결전문가_질문전송(questionValue);
alert(`${연결전문가.name}에게 질문이 등록되었어요.`);
}
async function handle새전문가질문제출(){
await 질문전송(questionValue);
alert("질문이 등록되었어요.");
}
async function handle약관동의팝업(){
const 약관동의 = await 약관동의_받아오기();
if(!약관동의){
await 약관동의_팝업열기();
}
}
위와 같이 한 가지 일만 하는, 명확한 이름의 함수로 만들면 필요한 상황에 따라 따로따로 부를 수 있다.
<button
onClick={async () => {
log('제출 버튼 클릭')
await openConfirm();
}}
/>
위 코드는 버튼 클릭 함수에 로그 찍는 함수와 API 콜이 섞여 있다.
이것을 로그는 버튼을 감싼 컴포넌트에서 찍고, 버튼 클릭 함수에서는 API 콜만 신경 쓰도록 바꿔보자.
<LogClick message='제출 버튼 클릭'>
<button onClick={openConfirm} />
</LogClick>
도대체 추상화가 정확히 뭔지 이해가 잘 안됐었는데, 토스 영상을 보고 어느 정도 틀은 잡은 것 같다🤩
다른 개발자가 컴포넌트를 사용처에서 선언적으로 사용할 수 있어야 한다.
컴포넌트가 어떤 로직을 수행하는지 내용을 알 수 있어야 한다.
무엇을 하는지만 외부에서 보여지고, 어떻게 하는지는 컴포넌트 내부에서 처리한다.
컴포넌트 추상화 예를 들어보자.
팝업 코드를 제로부터 디테일하게 구현한 예제가 있다.
<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}
/>
상황에 따라서 낮은 추상화 단계로 정리해도 무방하다.