[SLASH 21] - 실무에서 바로 쓰는 Frontend Clean Code

흑우·2023년 8월 12일

SLASH 21

목록 보기
2/2

실무에서 클린 코드의 의의

안 좋은 코드

  • 흐름 파악이 어렵고
  • 도메인 맥락 표현이 안 되어
  • 동료에게 물어봐야 알 수 있는 코드
  • 이러한 코드는 개발할 때 병목이 되고 유지보수할 때 시간이 오래 걸리며 심하면 기능 추가가 불가능한 상태 가 됩니다.
  • 성능도 안좋은 경우가 많아서 유저 입장에서도 쾌적하지 못하다.

실무에서 클린 코드의 의의 = 유지보수 시간의 단축 (코드 파악, 디버깅, 리뷰)

'처음엔' 클린했습니다.

  • 보통 기존 코드에 기능을 추가하는 상황에서 코드가 더러워집니다.

안일한 코드 추가의 함정

기능 추가 요청이 들어왔다!

  • 보험에 대한 질문을 입력하는 페이지가 있는데, 내 설계사가 있는 경우는 그 설계사 사진이 들어간 팝업을 먼저 띄워달라는 기능입니다.
  • 기존 코드
function QuestionPage(){
  async function handleQuestionSubmit(){
    const 약관동의 = await 약관동의_받아오기();
    if(!약관동의){
     await 약관동의_팝업열기(); 
    }
    await 질문전송(questionValue);
    alert("질문이 등록되었어요.")
  }
  return (
  	<main>
      <form>
       <textrea placeholder="어떤 내용이 궁금한가요?"/>
        <Button onClick={handleQuestionSubmit}>질문하기</Button>
      </form>	
    </main>
  )
}
  • 간단한 설계
function QuestionPage(){
  const [popupOpened, setPopupOpened] = useState(false);
  
  async function handleQuestionSubmit(){
  	const 연결전문가 = await 연결전문가_받아오기();
    if(연결전문가 !== null){
   	 setPopupOpened(true)
    } else {
     const 약관동의 = await 약관동의_받아오기();
      if(!약관동의) {
      	await 약관동의_팝업열기();
      }
      await 질문전송(questionValue);
      alert("질문이 등록되었어요")
    }
      
  }
  • 하나의 목적인 코드가 흩뿌려져 있습니다.

  • 하나의 함수가 여러 가지 일을 하고 있습니다.

  • 함수의 세부 구현 단계가 제각각입니다.

  • 큰 그림을 보며 코드 리팩토링

function QuestionPage(){
 const 연결전문가 = useFetch(연결전문가_받아오기);
  
  async function handleNewExpertQuestionSubmit(){
  	await 질문전송(questionValue);
    alert("질문이 등록되었어요");
  }
  
  async function handleMyExpertQuestionSubmit(){
  	await 연결전문가_질문전송(questionValue, 연결저문가.id);
    alert(`${연결전문가.name}에게 질문이 등록되었어요.`)
  }
}

return (
 <main>
      <form>
       <textrea placeholder="어떤 내용이 궁금한가요?"/>
        {연결전문가.connected ? (
        	<PopupTriggerButton
              popup={(
            	<연결전문가팝업
                  onButtonSubmit={handleMyExpertQuestionSubmit}
            )}> 질문하기</PopupTriggerButton>
        ): ()}
      </form>	
    </main>
)

로직을 빠르게 찾을 수 있는 코드

응집도

  • 같은 목적의 코드는 뭉쳐 두자
function QuestionPage(){
 const [popupOpened, setPopupOpened] = useState(false);
  async function handleClick() {
   setPopupOpened(true); 
  }
  function handlePopupSubmit(){
    await 질문전송(연결전문가.id);
    alert("질문을 전송했습니다.")
  }
  return (
  	  <>
        <button onClick={handleClick}>
          <Popup title="보험 질문하기" open={popupOpened}>
            <div>전문가가 설명드려요.</div>
            <button onClick={handlePopupSubmit}>확인</button>
          </Popup>
        </button>
      </>
  )
}

리팩토링 v1

  • 팝업을 조작하는 코드가 노란색 세 군데에 뚝뚝 떨어져 있다.
  • 파악도 한 번에 안되고 버그 발생 위험도가 높은 코드
function QuestionPage(){
	const [openPopup] = useMyExportPupup(연결전문가.id);
  
  function handleClick(){
   openPopup();
  }
  
  return <button onClick={handleClick}>질문하기</button>
}
  • openPopup 함수만 호출하면 popup 창을 열 수 있지만 오히려 코드 파악은 어려워 졌습니다.

  • 어떤 내용의 팝업을 띄우는지, 팝업에서 버튼을 눌렀을 때 어떤 액션을 하는지 모두 hook 속에 가려져서 파악할 수 없게 되었습니다.
    (대표적인 커스텀 훅의 안티 패턴)

    무엇을 뭉쳐야 하는가?

  • 뭉치면 쾌적 : 당장 몰라도 되는 디테일

  • 뭉치면 답답 : 코드 파악에 필수적인 핵심 정보

  • 클린 코드 != 짧은 코드 (중요하다!)

  • 클린 코드란 찾고 싶은 로직을 빠르게 찾을 수 있는 코드다.

핵심 데이터와 세부 구현 나누기

  • 남겨야 할 핵심 데이터와 숨겨도 될 세부구현을 나눠봅니다.
  • 핵심 데이터
    • 팝업 버튼 클릭 시 수행하는 액션
    • 팝업의 제목, 내용
  • 세부 구현
    • 팝업을 열고 닫을 때 사용하는 상태
    • 컴포넌트의 세부 마크업
    • 팝업의 버튼 클릭 시 특정 함수를 호출해야 한다는 바인딩

리팩토링 v2

function QuestionPage(){
 const [openPopup] = usePopup();
  async function handleClick(){
    const confirmed = await openPopup({
    	title: "보험 질문하기",
      	contents: <div>전문가가 설명드려요</div>
    })
    if(confirmed){
    	await submitQuestion();
    }
  }
  
  async function submitQuestion(연결전문가){
   await 질문전송(연결전문가.id);
    alert("질문을 전송했습니다.")
  }
  
  return <button onClick={handleClick}>질문하기</button>
}
  • 파악하기 쉬운 코드가 됩니다.

선언적 프로그래밍

  • 핵심 데이터만 전달받고 세부 구현은 뭉쳐 숨겨 두는 개발 스타일
  • "무엇을 해야할지만 알려줘, 세부 구현은 미리 해놨거든"
<Popup
  onSubmit={회원가입}
  onSuccess={프로필이동}
/>

명령형 프로그래밍

  • 어떻게 해야 할지 하나하나 명령하기
<Popup>
  <button onClick={async () => {
    const res = await 회원가입();
    if (res.success){
     프로필이름(); 
    }
    }}>전송</button>
  • 커스텀하기 쉽지만 읽는 데 오래 걸리고 재사용하기 어렵다.

선언적인 코드가 무조건 좋른 건가요?

  • 두 방법 모두 유동적으로 사용하시면 됩니다.

단일 책임

  • 하나의 일을 하는 뚜력한 이름의 함수를 만들자
async function handle[질문제출?](){
 const 약간동의 = await 약간동의_받아오기();
  if(!약간동의) {
   await 약간동의_팝업열기(); 
  }
  await 질문전송(questionValue);
  alert("질문이 등록되었어요.");
}
  • 함수 이름은 질문제출인데 내용에는 약관체크와 질문제출이 섞여 있다.
  • 여기서 기능이 추가된다면 더 복잡해진다.

리팩토링 Tip 1

  • 한 가지 일만 하는, 명확한 이름의 함수
async function handle연결전문가질문제출(){
  await 연결전문가_질문전송(questionValue);
  alert(`${연결전문가.name}`에게 질문이 등로되었어요.)
}

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

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

리팩토링 Tip 2

  • 한 가지 일만 하는, 기능성 컴포넌트
<button onClick={async () => {
  log("제출 버튼 클릭");
  await openConfirm();
  }}
  • 버튼 클릭 함수에 로그 찍는 함수와 API 콜이 섞여 있다.
<LogClick message="제출 버튼 클릭">
  <button onClick={openConfirm}>
  </button>
  </ LogClick>
  • LogClick이라는 컴포넌트를 만들어 버튼을 감싸고 버튼을 클릭하면 자동으로 클릭로그가 전송되도록 리팩토링
  • 버튼 클릭 함수에서는 API 콜만 신경 쓸 수 있다.
const targetRef = useRef(null);
useEffect(() => {
  const observer = new IntercectionObserver(
   ({ isIntersecting }) => {
    if (isIntersecting) {
     fetchCats(nextPage);
    }
   }
  )
  return () => {
    observer.unobserve(targetRef.current)
  }
}, [])

returb <div ref={targetRef}>더보기</div>
  • Impression 옵저버를 다는 코드 세부 구현과 API 콜을 하는 코드가 섞여 있다.
<IntersectionArea onImpression={() => fetchData(nextPage)}>
  <div>더 보기</div>
</IntersectionArea>
  • Impression 옵저버 세부 구현은 감싼 컴포넌트에 숨겨 두고, 사용하는 입장에서는 Impression 시 API콜만 신경쓴다.

리팩토링 Tip 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)
  • 중요 개념을 함수 이름에 담아 추상화

얼마나 추상화할 것인가?

  • Level 0
<button onClick={showConfirm}>
 전송
</button>
{isShowConfirm && (
  <Confirm onClick={() => {showMessage("성공")}}
)}
  • Level 1
<CofirmButton onConfirm={() => {showMessage("성공")}}> 전송 </CofirmButton>
  • Level 2
<CofirmButton message="성공">전송 </CofirmButton>
  • Level 3
<CofirmButton />
  • 정답은 없습니다. 원하는 만큼 추상화 하시면 됩니다.

실무 예시: 추상화할 수 있을까요?

  • 리뷰어: 혹시 이 부분을 통째로 추상화할 수 없을까요? await moreAccurateLocation.request()처럼요.
    꼭 모달을 연다고 하는 부분을 부모 컴포넌트가 알아야 하나 싶었습니다.

  • 코드 작성자: moreAccurateLocation.check()라는 이름은 어떠신가요?


  • 리뷰어: UltraCallProgress 와 UltraCallComparison이 비슷해 보이는데, 공통 부븐을 추상화할 수는 없을까요?

  • 코드 작성자: 이 부분은 아직까지 코드 중복이 심하지 않아. 추후 유연성을 생각했을 때 다르게 변할 수 있는 부분이 있을 것 같아 일부러 추상화를 깨기 했었는데요.
    (성급하게 추상화하다 보면, 유지보수에 어려움을 겪었던 적이 많았습니다) 조금만 더 고민해보겠습니다.

추상화 수준이 섞여 있으면 코드 파악이 어려워요.

<Title>별점을 매겨주세요</Title> // 높은 추상화
<div> // 낮은 추상화
  {STARS.map(() => <Star />)}
</div>
<Reviews /> // 높은 추상화
{rating !== 0 && ( // 중간 추상화
 <>
  <Argreement />
  <Button rating={rating} />
 </>
)}
  • 추상화 단계가 섞여 있어 코드를 읽는 것이 불편합니다.
<Title>별점을 매겨주세요</Title>
<Stars />
<Reviews /> 
<ArgreementButton show={rating !== 0}/>
  • 높은 추상화로 코드를 통일 (상황에 따라 낮은 추상화 코드로 통일해도 괜찮음)

액션 아이템

담대하게 기존 코드 수정하기

  • 두려워하지 말고 기존 코드를 씹고 뜯고 맛보고 즐기자!

큰 그림 보는 연습하기

  • 그 때는 맞고 지금은 틀리다. 기능 추가 자체는 클린해도, 전체적으로는 어지러울 수 있다.

팀과 함께 공감대 형성하기

  • 코드에 정답은 없습니다. 명시적으로 이야기를 하는 시간이 필요합니다.

문서로 적어보기

  • 글로 적어야 명확해집니다.
    • 향후 어떤 점에서 위험할 수 있는지
    • 어떻게 개선할 수 있는지

Reference

profile
흑우 모르는 흑우 없제~

0개의 댓글