ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Toss Frontend Fundamentals, 좋은 코드 그리고 나의 생각
    나의 개발 기록과 회고록 2025. 2. 18. 15:11

    최근 Toss에서 Frontend Fundamentals 라는 사이트를 오픈했다. 전지적 토스 프론트엔드 개발자 시점에서 좋은 코드에 대한 고민을 토스 공동체 분들과 고민하여 내린 결정들과 경험들을 공유하는 내용으로 보인다. 아직 자신의 코드에 대해서 확신이 없거나, 사수가 없는 환경, 처음 프론트엔드 개발을 시작하는 분들께는 너무 감사한 사이트가 아닐까 생각한다. 

     

    해당 아티클은 단순히 Toss Frontend Fundamentals 내용을 요약하고 정리하는 글 보다는 현업에서 일하면서 고민했던 부분, 이미 경험해서 공감 가는 부분 등 나의 생각을 적어둘 예정이다. 단순히 정보 전달이 아닌, 개인적인 생각을 기록하는 아티클이라고 생각하며 글을 시작한다. 

     

    * 인용 문구는 전부 Toss Frontend Fundamentals 문서를 참고했습니다.

    * 예시를 위한 코드 까지 가져오지 않고 링크로 대체합니다.

     

    좋은 코드에 대한 나의 생각

    변경하기 쉬운 코드
    좋은 프론트엔드 코드는 변경하기 쉬운 코드에요. 새로운 요구사항을 구현하고자 할 때, 기존 코드를 수정하고...

     

    내가 생각하는 좋은 코드는 엄격한 기준(Convention) 테두리 안에서 적당한 유연성을 머금고 있는 코드이다.

     

    엄격한 기준은 팀을 위한 코드라고 이야기할 수 있다. 최고의 성능을 가졌지만, 누구도 알아볼 수 없는 예술적인 코드를 함께 일하는 팀원이 보게 되면 어떤 생각을 가질까? 아마도 해당 코드를 해석하고 이해하는 과정에서 많은 리소스를 사용하게 되고 불필요한 의사소통이 많아질 것이다. 우리는 보통 팀을 꾸려서 같은 방향의 프로덕트를 만드는 일을 하기 때문에 팀이 합의한 기준을 엄격하게 지켜가며 코드를 작성해야 한다.

     

    적당한 유연성이 개인적으로 제일 어려운 부분이다. '이 컴포넌트는 어디까지의 자유도를 가져야할 까?' 매번 컴포넌트를 설계하는 과정에서 스스로에게 하는 질문이다. 'Prop이 너무 과하지 않을까?', 'Props drilling를 고려한 deps를 어디까지 가져가야 하지?', 'UI와 비즈니스 로직을 어느 레이어에서 분리하지?' 적당히 타협해서 개발을 완료하지만, 정답이 없는 싸움을 매번 하는 것 같다. 하지만 이런 고민들이 좋은 코드로 발전하는 과정임에는 분명하다. 

     

    Toss Frontend Fundamentals 에서는 가독성, 예측 가능성, 응집도, 결합도 4가지 기준으로 좋은 코드에 대해서 설명한다.

     

    가독성

    현업에서 레거시 코드를 만나게 되면 가장 먼저 코드의 의도 및 역할을 먼저 파악한다. 이런 과정이 자연스럽게 이어지고 이해가 빨리 가는 코드가 가독성이 좋다고 느껴진다. 보통 의도가 명확하고 일정한 범위 내에서 역할이 분담되어 있는 구조가 이해가 쉽다.

     

    1. 맥락 줄이기

    a. 같이 실행되지 않는 코드 분리하기 (코드 참고 : 링크)

    동시에 실행되지 않는 코드가 하나의 함수 또는 컴포넌트에 있으면, 동작을 한눈에 파악하기 어려워요. 구현 부분에 많은 숫자의 분기가 들어가서, 어떤 역할을 하는지 이해하기 어렵기도 해요.

     

    나는 '함께 실행되지 않는 코드'를 '책임을 분리할 수 있는 기준이 명확한 코드', '실행 조건이 다른 코드' 라고 이해했다. 유저의 분기 기준(Viewer User와 일반 User)으로 실행되는 함수의 요구사항이 명확히 다른 케이스이다. SubmitButton 컴포넌트의 역할은 유저의 권한에 따라 초대 유무를 결정하는 것이다. 하지만 예시 코드를 보면 조금 디테일한 요구사항들이 내포되어 있다. 

    아래의 기준을 확인해보자.

    • Viwer User
      • TextButton 형태의 버튼
      • disabled 상태
    • 일반 User
      • submit type의 일반 버튼
      • 에니메이션 재생(showButtonAnimation 함수 실행)

    명확한 기준(유저의 권한)에 따라서 행동하는 양식이 구분되는 요구사항이다. 각각의 요구사항이 구현된 각자의 버튼을 구현해서 분기해 준다면 가독성은 훨씬 높아진다. 토스 문서에서 제공한 예시에 일반적으로 코드를 인식하는 과정을 추가했다. 항상 생각하자 현업에서 만나는 코드는 단순히 함수 1~2개 그리고 분기가 1~2개로 끝나지 않는다. 수많은 api로직 복잡한 ui control 로직등이 섞여있는 상황을 고려해서 예시를 하나씩 지켜보자. 그리고 개선된 코드의 가치를 느껴보자.

    // 개선 전
    
    function SubmitButton() {
    	// 1. Viewer 권한을 확인해주는 코드구나~! 권한을 구분하는 로직이 필요하겠구나~!
      const isViewer = useRole() === "viewer";
    
      useEffect(() => {
    	// 2. Viwer 권한이면 애니메이션을 보여주지 않아야겠구나~! [분기 1스택]
        if (isViewer) {
          return;
        }
    	// 3. 일반 유저 권한이면 애니메이션을 실행해야겠구나~!
        showButtonAnimation();
      }, [isViewer]);
    
      return isViewer ? (
    	// 4. Viewer 권한인 경우 disable 상태의 Text 버튼을 노출하는구나 [분기 2스택]
        <TextButton disabled>Submit</TextButton>
      ) : (
    	// 5. 일반 유저 권한인 경우 submit 타입의 일반 Button 버튼을 노출하는구나
        <Button type="submit">Submit</Button>
      );
    }

     

    // 개선 후
    
    function SubmitButton() {
    	// 1. Viewer 권한을 체크하고 분기하는 로직이 있겠구나
      const isViewer = useRole() === "viewer";
    
    	// 2. Viewer User 권한에 따라 다른 기능을 가진 버튼으로 구성되어있네. 필요하면 내부 버튼을 파악해봐야지. 끝
      return isViewer ? <ViewerSubmitButton /> : <AdminSubmitButton />;
    }
    
    function ViewerSubmitButton() {
      return <TextButton disabled>Submit</TextButton>;
    }
    
    function AdminSubmitButton() {
      useEffect(() => {
        showAnimation();
      }, []);
    
      return <Button type="submit">Submit</Button>;
    }

     

    b. 구현 상세 추상화하기 (코드 참고 : 링크)

    한 사람이 코드를 읽을 때 동시에 고려할 수 있는 총맥락의 숫자는 제한되어 있다고 해요. 내 코드를 읽는 사람들이 코드를 쉽게 읽을 수 있도록 하기 위해서 불필요한 맥락을 추상화할 수 있어요.

     

    페이지 역할을 하는 컴포넌트는 설계 단계에서 주의해야 한다. 다양한 기능이 있을 가능성이 높고, 잠깐 방심해서 개발하다 보면 어디서 어디까지가 이 기능이고, 저 함수는 왜 여기 있지?라는 생각이 든다. 기능 구현에만 치중하다 보면 자주 생기는 실수이다. 이를 방지하기 위해서 컴포넌트, 함수, Hooks 등 다양한 방법으로 코드를 하나의 기능으로 추상화한다. 추상화 없이 다양한 기능(로그인 여부 체크, 회원가입, 상품 리스트, 배너, 다크모드, 메뉴 등) 메인 페이지에서 모두 구현되어야 한다고 생각해 보자. 가독성은 최악일 것이다.

     

    '구현 상세 추상화'라는 단어가 조금 어렵게 느껴져서 일상적인 예시에 대입해서 이해를 시도했다. 

    • '핸드 드립으로 뜨거운 블랙커피를 내렸어'라는 문장을 구체화해서 단계별로 설명해 보자.
      1. 커피콩을 글라인더로 갈아준다.
      2. 여과기에 여과용지를 깔아준다.
      3. 물을 주전자에 끓인다.
      4. 끓인 물을 여과 용지에 조금 부어서 적셔준다.
      5. 커피가루를 여과용지에 담아준다.
      6. 끓인 물을 부어 커피를 내린다.

    우리는 이 복잡한 과정을 한 문장으로 청자를 이해시킨다. 청자가 이해하기 충분한 추상화를 통해서 불필요한 의사소통을 줄일 수 있다. 코드도 마찬가지로 협업하는 동료에게 모든 코드를 노출시킬 필요가 없다. 어떤 의미인지 간략한 로직으로 설명할 수 있으면 충분하다. 기획서의 요구사항 하나를 구현하기 위해서 다양한 수행 작업이 필요하지만, 우리는 적당한 추상화 작업으로 가독성을 높일 수 있다.

     

    c. 로직 종류에 따라 합쳐진 함수 쪼개기 (코드 참고 : 링크)

    쿼리 파라미터, 상태, API 호출과 같은 로직의 종류에 따라서 함수나 컴포넌트 Hook을 만들지 마세요. 한 번에 다루는 맥락의 종류가 많아져서 이해하기 힘들고 수정하기 어려운 코드가 돼요.

     

    위 a,b에서는 코드를 분리하고 하나로 합치는 과정에 대해서 고민했다면, 이번에는 잘못된 기준으로 코드를 합치는 경우를 경고한다. 보통 React에서 로직을 하나로 합치는 경우 Hooks를 사용한다. '그럼 위에서 말한 대로 API 로직이나 상태 관리 로직들도 공통된 기준으로 하나의 Hook으로 묶으면 좋은 거 아냐?'라고 오류를 범할 수 있다.

     

    리액트에서 좋은 성능을 위해서 리렌더링을 고려한다. 만약 변동 빈도가 높은 상태 값이나 로직을 하나의 Hook에 모아두고 사용한다면, 단순히 파라미터 하나 수정하는 순간 리렌더링이 불필요하게 발생할 것이다. 따라서 추상화나 응집도를 고려해서 코드를 하나로 뭉치더라도 이 부분을 고려해서 설계를 해야 한다. 그럼 어떤 부분을 모아야 할까?

     

    나의 경험을 통해서 내린 기준은 불변성 여부를 먼저 판단하는 것이다. 비디오 플레이어로 예를 들어보자.

    • 어떠한 Video Player를 사용하더라도 보편적으로 잘 변하지 않는 속성들을 살펴보자.
      • 기능 : Play, Stop, Seek, Seeking, Time Stamp, Speed +/- , Ad(광고) 등
      • 상태 : Time, Status(playing/seek/stop), Speed, 자막 노출 여부
      • 기타 비즈니스 로직(Share, Download...)

    보편적으로 변하지 않는 기능, 상태, 로직들이 있다면 공통 Hooks로 관리하면 편하게 유지보수를 할 수 있었다. 이런 방식으로 결합한다면 로직을 추가하는 경우에도 기존의 상태값이나 로직들이 필요한 경우가 많게 되고, 다른 컴포넌트나 hooks를 찾아다니는 수고를 덜어준다.

     

    정리하면, 특정한 카테고리의 공통점을 기준으로 무조건 합치는 개발을 좋은 방법이 아니다. 합치고 분리하는 과정에서 성능, 개발 편의성, 가독성 등의 고민과 함께 설계를 해야 한다. 위의 예시는 변동성을 기준으로 보편적인 로직과 상태를 결합한 예시이다. 

     

    예측 가능성

    함께 협업하는 동료들이 함수나 컴포넌트의 동작을 얼마나 예측할 수 있는지를 말해요. 예측 가능성이 높은 코드는 일관적인 규칙을 따르고, 함수나 컴포넌트의 이름과 파라미터, 반환 값만 보고도 어떤 동작을 하는지 알 수 있어요.

     

    * 해당 카테고리는 문서 내용의 예시와 설명을 보면 충분히 이해할 수 있는 내용들이라서 간단한 코멘트만 남깁니다.

     

    a. 이름 겹치지 않게 관리하기 (코드 참고 : 링크)

    같은 이름을 가지는 함수나 변수는 동일한 동작을 해야 해요. 작은 동작 차이가 코드의 예측 가능성을 낮추고, 코드를 읽는 사람에게 혼란을 줄 수 있어요.

     

    보편적으로 사용되는 http 서비스나 common 한 코드들은 더욱 네이밍에 주의해야 한다. 협업하는 개발자가 함수나 변수의 이름을 보고 한 번에 이해할 수 있다면, 네이밍이 조금 더 길어지는 게 크게 아쉽지 않다. 

     

    b. 같은 종류의 함수는 반환 타입 통일하기 (코드 참고 : 링크)

    협업하는 과정에서 common, util, query 등 공통의 기능을 가진 로직들을 한 카테고리(디렉토리) 안에서 관리하는 경우가 있다. 이런 경우에는 반환하는 타입에 대해서는 통일하는 것이 기본이다. 문서에서 API Query 로직을 Hook으로 관리한 사례를 바탕으로 설명한다. 공통의 목적을 가지고 특정 네이밍이 통일성을 갖거나, 같은 동작을 반복하는 템플릿 코드를 관리하는 경우 반환 타입이 다르다면 매번 해당 코드의 파일을 조회하여 확인해야 한다. 

     

    c. 숨은 로직 드러내기 (코드 참고 : 링크)

    기본적으로 함수를 선언할 때, 함수의 역할이 중첩이 되거나 복잡한 로직이 있다면 가독성이 떨어지고 예측할 수 없는 오류를 야기한다. 함수의 네임에 적합한 하나의 기능을 구현하는 것이 좋다.

     

    응집도

    응집도란, 수정되어야 할 코드가 항상 같이 수정되는지를 말해요. 응집도가 높은 코드는 코드의 한 부분을 수정해도 의도치 않게 다른 부분에서 오류가 발생하지 않아요. 함께 수정되어야 할 부분이 반드시 함께 수정되도록 구조적으로 뒷받침되기 때문이죠.

    가독성과 음집도는 서로 상충할 수 있어요.
    일반적으로 응집도를 높이기 위해서는 변수나 함수를 추상화하는 등 가독성을 떨어뜨리는 결정을 해야 해요. 위험성이 높지 않은 경우에는, 가독성을 우선하여 코드 중복을 허용하세요.

     

    a. 함께 수정되는 파일을 같은 디렉토리에 두기

    프로젝트에서 코드를 작성하다 보면 Hook, 컴포넌트, 유틸리티 함수 등을 여러 파일로 나누어서 관리하게 돼요. 이런 파일들을 쉽게 만들고, 찾고, 삭제할 수 있도록 올바른 디렉토리 구조를 갖추는 것이 중요해요.

    함께 수정되는 소스 파일을 하나의 디렉토리에 배치하면 코드의 의존 관계를 명확하게 드러낼 수 있어요. 그래서 참조하면 안 되는 파일을 함부로 참조하는 것을 막고, 연관된 파일들을 한 번에 삭제할 수 있어요.

     

    프로젝트의 크기가 커지면 자연스럽게 도메인이나 서비스별로 분리해서 관리하는 방법이 필요하다. 최근 FSD 디자인 패턴이 인기가 많아진 계기도 마찬가지다. 규모가 작거나 속도가 중요한 경우에는 어려울 수 있지만, 장기적으로 꾸준히 유지보수를 하면서 제품을 만들어가야 하는 경우, 특정한 단위(서비스, 도메인 등)로 분리해서 한 디렉토리로 관리하면 다양한 장점이 있다.

     

    • 특정 서비스, 도메인만 작업하는 개발자의 관점에서 코드를 찾기 위해 다른 디렉토리를 돌아다닐 필요가 없다.
    • 운영 기간이 끝난 경우 특정 부분만 제거하더라도 해당 도메인 외에 참조되는 부분이 없기 때문에 도려내기 쉽다.

    b. 폼의 응집도 생각하기

    프런트엔드 개발을 하다 보면 Form으로 사용자에게 값을 입력받아야 하는 경우가 많아요. Form을 관리할 때는 2가지의 방법으로 응집도를 관리해서, 함께 수정되어야 할 코드가 함께 수정되도록 할 수 있어요.

     

    다른 기능과 다르게 폼을 다루는 로직에서는 설계를 디테일하게 해야 코드를 치는 과정이 수월해진다. 미리 만들어진 Control 적용이 가능한 Input Component가 있더라도, 기존의 요구사항이 다른 경우 새롭게 개발해야 하거나 폼 조합을 다시 해야 할 수 있다. 문서에서 제시하는 폼 필드와 폼 전체 단위 응집도에 대한 가이드라인이 설계에 많은 도움을 줄 것이라고 생각한다. 

     

    결합도

    결합도란, 코드를 수정했을 때의 영향범위를 말해요. 코드를 수정했을 때 영향범위가 적어서, 변경에 따른 범위를 예측할 수 있는 코드가 수정하기 쉬운 코드에요.

     

    a. 중복 코드 허용하기

    개발자로서 여러 페이지나 컴포넌트에 걸친 중복 코드를 하나의 Hook이나 컴포넌트로 공통화하는 경우가 많아요. 중복 코드를 하나의 컴포넌트나 Hook으로 공통화하면, 좋은 코드의 특징 중 하나인 응집도를 챙겨서, 함께 수정되어야 할 코드들을 한꺼번에 수정할 수 있어요.
    그렇지만, 불필요한 결합도가 생겨서, 공통 컴포넌트나 Hook을 수정함에 따라 영향을 받는 코드의 범위가 넓어져서, 오히려 수정이 어려워질 수도 있어요.
    처음에는 비슷하게 동작한다고 생각해서 공통화한 코드가, 이후 페이지마다 다른 특이한 요구사항이 생겨서, 점점 복잡해질 수 있어요. 동시에 공통 코드를 수정할 때마다, 그 코드에 의존하는 코드들을 일일이 제대로 테스트해야 해서, 오히려 코드 수정이 어려워지기도 하죠.

     

    여러 페이지나 컴포넌트에서 사용하는 로직을 하나의 Hook으로 관리하면 중복된 코드를 방지할 수 있다. 중복된 코드를 방지하는 행위 자체는 가독성이나 유지보수 측면에서 좋은 방법이다. 하지만 많은 곳에서 사용하는 코드가 변경돼야 하는 상황이 오거나 한 페이지에서 해당 코드를 실수로 수정하게 되어서 해당 Hook을 사용하는 페이지에서 사이드 이펙트를 유발한다면 이야기가 달라진다. 이런 경우 중복된 코드를 어느 정도 허용해야 한다. 

    항상 고민하는 부분이지만 '어느 정도'가 도대체 어느 정도 일까?

     

    정답은 없겠지만, 최근에는 도메인이나 서비스를 기준으로 디렉토리 관리를 하게되면 결합도를 조금 줄일 수 있는 공통화 작업이 될 것 같다고 느낀다. 도메인 자체나 기능상의 특수성(비디오, 결제, 인증 등)을 기준으로 분리를 이미 했다면, 해당 디렉토리 범위 안에서는 크게 변동성이 없다고 느껴지는 경우 결합도가 올라가더라도 공통화 작업을 하는 것 같다. 하지만 기준이 따로 없거나 컨벤션이 없는 환경이라면 중복된 코드를 허용하는 편이 더 좋은 방법일 수 있다.

     

    마음으로는 이해하는데 머리로는 이해가 안 되는 작업 같은 느낌이다. 항상 어렵다. 

     

    b. Props Drilling 지우기

    Props Drilling은 부모 컴포넌트와 자식 컴포넌트 사이에 결합도가 생겼다는 것을 나타내는 명확한 표시예요. 만약에 Drilling 되는 name prop의 이름이 firstName으로 변경되면, 해당 prop을 참조하는 모든 컴포넌트를 수정해야 해요.

     

    Props Drilling은 컴포넌트를 설계하는 과정에서 자주 출몰하는 이슈이다. 몇 가지 규칙을 정해두면 복잡한 컴포넌트를 설계하는 과정이 조금 편안해진다. 

    • Props Drilling은 2deps까지만 허용한다. 
    • 3deps 이상의 컴포넌트 강결합이 필요하다면 Props가 아닌 Context로 상태를 관리하는 것이 효율적이다.
    • 애매한 Props를 Context로 관리하지 않는다. 한 Context에서 상태가 결합 없이 사용해야 하는 경우에만 추가한다. 이외에는 Props로 관리한다. 
    • 작업 규모가 크거나 커질 가능성이 있는 경우 미리 Context로 관리하거나 추가 리소스를 투여해 디테일한 설계 후에 코딩을 한다.

    문서에서 제시하는 Composition과 Context API 사용하는 방법으로 충분하지만, 팀이나 본인만의 컨벤션이 있다면 고민하는 시간을 단축시킬 수 있다. 

    그래서 좋은 코드가 뭔데?

    앞에서 이론적인 내용이나 경험을 통한 생각을 전달했으니 단순히 나의 생각에 대해서 공유하고 싶다. Toss Frontend Fundamentals 문서를 작성해 주신 개발자 분들이 '이게 정답이니까 이걸 따라서 개발하면 최고의 코드를 작성할 수 있어!'라고 생각해서 프로젝트를 배포한 것이 아마 아닐 거다. '좋은 코드가 뭔지 계속 고민하고 의견을 나누다 보니 우리가 공통적으로 생각한 방향성은 이거예요. 더 좋은 방법이 있으면 함께 공유했으면 좋겠어요.'라는 의견에 가까울 것이다. 개념이나 개발 경험에 대한 내용은 앞에 내용에 공유했으니 조금 더 주관적인 관점에서 좋은 코드에 대한 생각을 전달하고 싶다.

     

    이에 대한 주관적인 나의 답은, 

    • 비즈니스 요구사항에 맞춰 100%에 가까운 결과물
    • 클라이언트 요구 일정을 준수한 결과물
    • 혼자가 아닌 함께 일하는 동료 개발자, 협업하는 타 부서 팀원을 고려한 결과물
    • 자신이 맡은 피쳐를 완벽하게 개발하기 어렵다면 빠르게 팀에 공유하여, 일정과 요구사항에 맞춰 개발한 결과물

    내가 지금까지 현업에서 선배들과 동료들에게 배우고 경험했던 좋은 코드의 정의이다. 어쩔 때는 문서에서 제시하는 모든 기준을 어기고 빠르게 코드를 작성하는 방법이 어떠한 상황 속에서는 최고의 코드일지 모른다. 앞으로도 계속 어렵고, 아쉽고, 완벽함을 느끼지 못하더라도 계속 고민하는 태도가 좋은 코드를 작성하는 길이 아닐까 싶다. 

     

    주니어 입장에서 이러한 문서는 많은 영감을 줄 수 있다고 생각한다. Toss Frontend Fundamentals를 위해 고생하신 모든 분께 감사함을 전달하고 싶다. 

Designed by Tistory.