프론트엔드/React

실무에서 바로 쓰는 Front-end Clean Code (React ver.)

홍수성찬 2021. 5. 9. 19:50

클린 코드란 '명확한 이름', '중복 줄이기'를 먼저 말한다. 

 

실무에서 클린 코드의 의의

그 코드는 건드리지 않는 게 좋겠어요. 제가 할게요.

이 말의 의미를 다시 한 번 해석해보면

 

1. 흐름 파악이 어렵고

2. 도메인 맥략 표현이 안되어

3. 동료에게 물어봐야 알 수 있는 코드   

 

이러한 코드는 유지 보수도 힘들고, 최악의 경우에는 추가 기능을 추가하지 못할 수도 있다. 그리고 윶어 입장에서도 쾌적하지 못할 수 있다.

 

여기서 클린 코드란, 유지 보수 시간의 단축이라고 생각하면 된다. 클린 코드로 작성을 하면 코드를 파악하기 쉽고, 디버깅 시간도 단축되며 낭비하는 시간을 줄일 수 있다.

 

안일한 코드 추가의 함정

만약에 기능을 추가해야 할 일이 생기면 어떻게 될까? 다른 사람이 작성한 코드에서 내가 새로운 기능을 추가해야 할 때가 있을 수 있다. 이럴 경우, 문제는 코드가 흩뿌려져 있을 수 있어서 스크롤을 자주 움직이며 개발해야 할 일이 생길 수 있다. 그리고 하나의 함수가 여러 가지의 일을 하게 만들 수 있다. 이를 주의해서 코드를 작성해야 한다.

 

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

* 함수 세부 구현 단계 통일   

같은 역할을 하는 코드를 하나로 뭉쳐서 코드를 작성하는 것이 좋다.   

* 함수가 한 가지 일만 하도록 쪼개기   

말 그대로 하나의 함수가 하나의 역할을 할 수 있도록 만들어야 한다. 하나의 함수가 여러 가지의 일을 하도록 만드는 것은 좋은 코드가 아니라고 볼 수 있다.   

 

코드가 길어졌다?

코드가 길어졌으니까 클린 코드가 아니다. 이는 잘못된 말이다. <b>클린 코드</b>는 길이보다 원하는 로직을 빠르게 찾을 수 있는 코드라는 것을 알아야 한다.

 

클린 코드 != 짧은 코드

 

원하는 로직을 빠르게 찾으려면 어떻게 만들어야 할까?   

 

1. 응집도 - 하나의 목적을 가진 코드를 뭉치자.   

2. 단일 책임 - 하나의 함수가 하나의 역할을 하도록 만들자.   

3. 추상화 - 함수의 핵심 개념을 필요한 만큼만 보이도록 하자.   

 

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

1. 응집도   

function QuestionPage() {
    const [popupOpened, setPopupOpened] = useState(false); // 1
    async function handleClick() {
        setPopupOpened(true);
    }

    function handlePopupSubmit() { // 2
        await 질문전송(연결전문가.id);
        alert("질문을 전송했습니다.")
    }

    return (
        <>
            <button onClick={handleClick}>질문하기</button>
            <Popup title="보험 질문하기" open={popupOpened}> // 3
                <div>전문가가 설명드려요.</div>
                <button onClick={handlePopupSubmit}>확인</button>
            </Popup>
        </>
    )
}

위 코드에서 보듯이, 1번 2번 3번은 같은 역할을 하는 코드이다. 그러나 서로 띄엄띄엄 있기 때문에 한 번에 파악하기 힘들고 나중에는 버그 발생의 위험도 가지고 있다. 

 

function QuestionPage() {
    const [openPopup] = useMyExpertPopup(연결전문가.id);

    function handleClick() {
        openPopup();
    }

    return <button onClick={handleClick}>질문하기</button>;
}

위 처럼 커스텀 훅을 사용해서 한 군데로 뭉칠 수 있다. openPopup 함수만 호출하여 커스텀 팝업을 열 수 있도록 만들 수 있다. 그러나 오히려 더 읽기 힘든 코드가 되었다. 어떤 팝업을 여는지 알 수 있어야 하지만, 훅 속에 가려져서 알 수 없게 되었다.   

 

그렇다면 무엇을 뭉쳐야 할까?

* 당장 몰라도 되는 디테일 *

 

반대로 코드 파악에 필수적인 핵심 정보를 뭉치면 오히려 답답할 수 있다.   

 

결론은, 뭉쳐서 짧은 코드로 만든다고 코드가 깨끗한 것은 아니다. 찾고 싶은 로직을 빠르게 찾을 수 있는 코드가 클린 코드이다.   

 

다시 돌아와서, 그렇다면, 위에 코드를 어떻게 리팩토링을 해야 할까?

function QuestionPage() {
    const [popupOpened] = 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>;
}

핵심 데이터만 남기고 세부 구현을 숨겨 파악하기 쉬운 코드로 만들 수 있다. openPopup이라는 커스텀 훅 안에 모든 코드를 다 숨기는 것이 아니라, 세부 구현만 숨겨 놓고, 핵심 데이터인 팝업 제목, 내용, 액션은 바깥에서 넘기는 코드로 작성하여 세부 구현을 읽지 않고도 어떤 팝업인지 파악할 수 있다.   

 

선언적 프로그래밍

핵심 데이터만 전달받고 세부 구현은 뭉쳐 숨겨 두는 개발 스타일이다. 즉, 무엇을 하는 함수인지 빠르게 이해가 가능하다는 특징을 가지고 있다. 그리고 세부 구현은 내부에 뭉쳐 둔다는 특징도 가지고 있다.

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

또한, 무엇만 바꿔서 쉽게 재사용이 가능하다.

<Popup onSubmit={회원가입} onSuccess={프로필로 이동}/>

만약에 뭉치지 않았다면?

 

명령형 프로그래밍

어떻게 해야 할지 하나하나 명령하여 세부 구현을 작성하는 방식이다.

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

이는 노출되어 있기 때문에 커스텀하기 쉽지만, 읽는 데 오래 걸리고 재사용하기 어렵다는 단점을 가지고 있다.   

 

단일 책임

하나의 일을 하는 뚜렷한 이름의 함수를 만드는 것을 의미한다.

async function ?() {
    const 약관동의 = await 약관동의_받아오기();

    if(!약관동의) {
        await 약관동의_팝업열기();
    }

    await 질문전송(questionValue);
    alert("질문이 등록됐어요.");
}

위 함수의 이름을 어떻게 지어야 할까? 우선 중요 포인트가 모두 담겨 있지 않은 함수명은 위험하다. (예. handle질문제출) 위 코드는, 약관 체크 후 팝업과 질문 제출의 역할을 가지고 있기 때문에 모두 포함한 이름을 선택해야 한다. 만약에 여기서 기능이 추가되었다면 어떻게 될까?

 

async function ?() {
    const 약관동의 = await 약관동의_받아오기();

    if(!약관동의) {
        await 약관동의_팝업열기();
    }

    await 질문전송(questionValue);
    alert("질문이 등록됐어요.");

    const 연결전문가 = await 연결전문가_받아오기();
    if(연결전문가 !== null) {
        await 연결전문가_질문전송(questionValue);
        alert(`${연결전문가.name}에게 질문이 등록됐어요.`);
    }
}

위 코드는 이전 코드에서 연결전문가 기능이 추가되었다. 코드가 더 뚱뚱해지고 handle질문제출이라는 이름에서 질문제출 외 두 가지 기능을 더 담당하고 있다.   

 

이제 함수 내 여러 가지 역할을 맡았던 것을 쪼개보는 연습을 하자.

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를 호출하는 역할을 하고 있다. 이 점은 조금 아쉽다.

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

위 처럼, 로그는 버튼을 감싼 컴포넌트에서 찍고, 버튼 클릭함수에서는 API를 호출하는 역할만 한다.

 

조건이 많아지면 한글 이름도 유용하다.

const 패널티풀림 = reasons.indexOf('PENALTY') > -1;
const 평점4점이상 = review.rate >= 80;

if(패널티풀림) {
    return
}
if(평점4점이상) {
    return
}
const 설계사정보팝업_노출 = 12345;
const 설계사정보팝업_확인 = 56789;

const handleMatchPlanner = async () => {
    log(설계사정보팝업_노출);
    const confirmed = await openConfirm();
    if(confirmed) {
        log(설계사정보팝업_확인);
        goToChat(plannerId);
    }
};

도메인이 복잡하여 영어 이름을 길게 짓는 게 복잡도를 높이는 경우, 상수를 직관적으로 보고 싶을 때, 복잡한 조건문이 많아질 때, 한글 변수명은 굉장히 유용하다.   

 

추상화

핵심 개념을 뽑아내는 것을 의미한다. 즉, 로직에서 핵심 개념을 가져오는 것이다.

 

프론트엔드 코드의 추상화 - 컴포넌트

<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)

위 코드는 중요 개념만 남기고 추상화한 것이다.

 

그렇다면 얼마나 추상화할 것인가?

 

1. 0단계

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

버튼을 클릭하면 Confirm 창을 띄우고, 클릭하면 특정 메세지를 띄우는 구체적인 코드이다.   

 

2. 1단계

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

컴포넌트를 추상화한 것이다. onConfirm을 통해서 원하는 액션을 넘길 수 있다.

 

3. 2단계

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

message라는 prop만 넘겨서 Confirm 창에 원하는 메세지를 보여주도록 더 간단하게 추상화한 코드이다.   

 

4. 3단계

<ConfirmButton />

더 나아가서 ConfirmButton의 이름 아래에서 모두 추상화 시킬 수 있다.

 

추상화 수준이 섞여 있으면 코드 파악이 어렵다.

<Title>별점을 매겨주세요</Title> // 높은 추상화
<div>
    {STARS.map(() => <Star />)} // 낮은 추상화
</div>
<Review /> // 높은 추상화
{rating !== 0 && ( // 중간 추상화
    <>
        <Agreement />
        <Button rating={rating} />
    </>
)}

위 코드는 전체적인 코드가 어느 수준으로 구체적으로 기술된지 파악할 수 없다. 위 처럼 높은 추상화, 낮은 추상화가 섞여 있다면 코드 파악이 어려워진다.

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

위 처럼, 추상화 단계를 비슷하게 정리하여 추상화 수준이 높은 것끼리, 혹은 낮은 것끼리 모은다면 쉽게 이해할 수 있을 것이다.

 

액션 아이템

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

두려워하지 말고 기존 코드를 수정하여 좋은 코드를 만들어보자.   

 

2. 큰 그림 보는 연습하기   

기능 추가 자체는 깨끗해보여도, 전체적으로는 어지러워 보일 수 있다.   

 

3. 팀과 함께 공감대 형성하기   

코드에는 정답이 따로 있지 않다. 명시적으로 이야기를 하는 시간이 필요하다.   

 

4. 문서로 적어 보기   

글로 적어야 명확해진다. 향후 어떤 점에서 위험할 수 있는지, 어떻게 개선할 수 있는지 명확하게 파악 후, 코드를 수정해 나갈 수 있다.