Hwirin-Kim 님의 상세페이지[1팀 김휘린] Chapter 2-1. 클린코드와 리팩토링

과제 체크포인트

배포링크

https://hwirin-kim.github.io/front_6th_chapter2-1/

기본과제

  • 코드가 Prettier를 통해 일관된 포맷팅이 적용되어 있는가?
  • 적절한 줄바꿈과 주석을 사용하여 코드의 논리적 단위를 명확히 구분했는가?
  • 변수명과 함수명이 그 역할을 명확히 나타내며, 일관된 네이밍 규칙을 따르는가?
  • 매직 넘버와 문자열을 의미 있는 상수로 추출했는가?
  • 중복 코드를 제거하고 재사용 가능한 형태로 리팩토링했는가?
  • 함수가 단일 책임 원칙을 따르며, 한 가지 작업만 수행하는가?
  • 조건문과 반복문이 간결하고 명확한가? 복잡한 조건을 함수로 추출했는가?
  • 코드의 배치가 의존성과 실행 흐름에 따라 논리적으로 구성되어 있는가?
  • 연관된 코드를 의미 있는 함수나 모듈로 그룹화했는가?
  • ES6+ 문법을 활용하여 코드를 더 간결하고 명확하게 작성했는가?
  • 전역 상태와 부수 효과(side effects)를 최소화했는가?
  • 에러 처리와 예외 상황을 명확히 고려하고 처리했는가?
  • 코드 자체가 자기 문서화되어 있어, 주석 없이도 의도를 파악할 수 있는가?
  • 비즈니스 로직과 UI 로직이 적절히 분리되어 있는가?
  • 코드의 각 부분이 테스트 가능하도록 구조화되어 있는가?
  • 성능 개선을 위해 불필요한 연산이나 렌더링을 제거했는가?
  • 새로운 기능 추가나 변경이 기존 코드에 미치는 영향을 최소화했는가?
  • 코드 리뷰를 통해 다른 개발자들의 피드백을 반영하고 개선했는가?
  • (핵심!) 리팩토링 시 기존 기능을 그대로 유지하면서 점진적으로 개선했는가?

심화과제

  • 변경한 구조와 코드가 기존의 코드보다 가독성이 높고 이해하기 쉬운가?
  • 변경한 구조와 코드가 기존의 코드보다 기능을 수정하거나 확장하기에 용이한가?
  • 변경한 구조와 코드가 기존의 코드보다 테스트를 하기에 더 용이한가?
  • 변경한 구조와 코드가 기존의 모든 기능은 그대로 유지했는가?
  • (핵심!) 변경한 구조와 코드를 새로운 한번에 새로만들지 않고 점진적으로 개선했는가?

과제 셀프회고

이번 basic과제를 완료하고 눈으로 동작이 다 잘되는것을 확인 한 후 테스트를 마지막으로 돌려보니 딱 하나의 테스트 케이스가 동작하지 않는걸 발견했습니다.

"진짜 이제 다했다!!" 라는 생각이였다가 막혀서 그런지 실제로 한 시간 정도 막혀있었는데 체감은 두 세시간은 막혀있던 기분이였습니다.

문제는 innerHTML로 DOM이 통째로 재렌더링되면서 버튼 요소가 새로 생성되는데, 테스트 코드에서 버튼을 변수로 캐시해두면 해당 버튼을 누르지 않아 제대로 동작하지 않는것을 발견했습니다. 그래서 버튼과 수량 요소는 클릭 직전에 querySelector로 다시 가져와야 정상 작동한다는 것을 배웠는데 그것을 새벽에 블로그에 잘 정리해뒀습니다.. 링크 : https://huirin.tistory.com/250

과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?

  1. 첫 번째로 과제를 진행하며 제일 신경쓴 부분은 내가 의도하고 변경하고자 하는 부분을 변경하고 나서 테스트코드로 검증을 하는것이였습니다. 너무 별거 아니면서 충분히 예측 가능하다고 하더라도 일정 부분 수정을 하고 나면 반드시 테스트로 검증을 진행했습니다.

  2. 두 번째로 신경 쓴 부분은 너무 한번에 다 바꾸려고 하지 않기였습니다. 실제로 제일 처음엔 UI를 바로 분리시키고 값들을 나중에 넣어도 되지 않을까? 라고 생각했지만,, 너무 강하게 결합되어있는 UI와 비즈니스로직들에 의해 AI도 해결책을 주지 못하였습니다.

그렇게 한 번 완전히 첫 커밋으로 돌아가 이번엔 스토어를 만들고 상태부터 분리하려고 하였습니다. 그러나 스토어를 만드는것 역시 쉽지만은 않았는데, 코드 양이 너무 많아서 어디에서 어떻게 사용되고 있는지 잘 알지 못했기 때문에 어디에서 어떤 액션함수를 만들어서 써야할지 한 눈에 보이지 않았기 때문입니다.

그렇게 시행착오를 겪다가 마지막으로 시도한 방법은 스토어와 유사한 모양이지만, 그냥 단순히 객체야! 하는 느낌으로 전역변수 부분의 형태만 객체 형태로 바꾸고, 그에 알맞게 사용 하고 있는 곳을 따라가서 이름만 바꿔줬습니다.

export const Store = {
  products: {
    list: [
      {
        id: productIds.p1,
        name: "버그 없애는 키보드",
        val: 10000,
        originalVal: 10000,
        q: 50,
        onSale: false,
        suggestSale: false,
      },
      {
        id: productIds.p2,
        name: "생산성 폭발 마우스",
        val: 20000,
        originalVal: 20000,
        q: 30,
        onSale: false,
        suggestSale: false,
      },
      {
        id: productIds.p3,
        name: "거북목 탈출 모니터암",
        val: 30000,
        originalVal: 30000,
        q: 20,
        onSale: false,
        suggestSale: false,
      },
      {
        id: productIds.p4,
        name: "에러 방지 노트북 파우치",
        val: 15000,
        originalVal: 15000,
        q: 0,
        onSale: false,
        suggestSale: false,
      },
      {
        id: productIds.p5,
        name: `코딩할 때 듣는 Lo-Fi 스피커`,
        val: 25000,
        originalVal: 25000,
        q: 10,
        onSale: false,
        suggestSale: false,
      },
    ],
  },
  ui: {
    selectedProductId: null,
    cartItems: [],
    totalAmount: 0,
    itemCount: 0,
  },
};

위처럼 최초에는 상태관리 스토어처럼 액션에 의해 변경해야하거나 그렇지 않고, 전역에 선언된 객체일 뿐이였습니다. 그치만 그렇게라도 하고나니 조금씩 분리할 수 있는 단위도 보였고, "굳이 DOM에서 가져와야 하는 부분인가??" 하는 부분들도 보이기 시작했습니다.

그 부분이 보이기 시작할 때 부터 스토어의 필요성을 느꼈고,

export const ACTION_TYPE = {
  QUERY: "QUERY",
};

/**
 * 스토어 생성
 * @param {Object} initialState - 초기 상태
 * @param {Object} actions - 액션 함수들
 * @returns {Object} 스토어 객체
 */
export const createStore = (initialState, actions = {}) => {
  let state = initialState;
  const listeners = [];

  // 스토어 객체 생성
  const store = {
    getState: () => state,
    setState: (newState) => {
      state = newState;
      listeners.forEach((listener) => listener());
    },
    subscribe: (listener) => {
      listeners.push(listener);
      return () => {
        listeners = listeners.filter((l) => l !== listener);
      };
    },
  };

  // action 함수들을 store에 바인딩
  const boundActions = {};
  Object.keys(actions).forEach((actionName) => {
    boundActions[actionName] = (...args) => {
      const result = actions[actionName](state, ...args);

      if (!result) return;

      // 모든 액션은 상태 변경을 위한 것이므로 결과를 새로운 state로 설정
      if (typeof result === "object" && result.type === "QUERY") {
        return result.data;
      }
      store.setState(result);
      return result;
    };
  });
  return {
    ...store,
    ...boundActions,
  };
};

단순한 전역 객체에서 시작한 스토어는, 점차 상태와 로직을 분리할 필요가 생기며 위와같이 createStore 구조로 발전했습니다. 그 후 액션 함수에서 조회 기능도 필요해지면서 ACTION_TYPE.QUERY 같은 프로토콜을 붙이게 되었고, 점차 미니 상태 관리 라이브러리처럼 자라나게 되었습니다.

이처럼 처음부터 모든걸 하려고 했다면 보이지 않았을 것들을 작게보면 잘 보인다는것을 깨달았고, 다시 한번 요약해보면 가장 중요하다고 느낀것은 *테스트이고 그 다음은 작은 단위부터 변경하기 입니다!

과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?

####일단 회고를 한번 해보겠습니다.. 사실 이번 과제를 진행하면서 DOM에서 정보를 얻어오는 대신 스토어를 만들어서 스토어로 데이터를 보관하는 역할을 만드는데 4일, 그리고 이제 역할이 줄어든 DOM을 분리하는데는 5시간이 채 안걸린것 같습니다. 물론 4일 안에는 수많은 리셋과 그 만큼 어떤 방향이 좋은 방향인지를 찾아가는 시간이 제일 많이 들었고, DOM에서 어떤 정보를 얻어서 비즈니스로직을 만드는 코드들을 분리하는데 두 번째로 많은 시간이 들었습니다. 이 부분들은 특히 AI를 거의 쓰지 않고 진행하여 더욱 시간이 많이 들었는데, 오히려 AI를 쓰지 않을 때 부터 작은 단위로 분석하기 시작하여 집중도도 높아지고 무언가 해결이 되는 기분이 들어 몰입이 되는 시간이였습니다.

그렇게 DOM과 비즈니스로직을 어느정도 떼어낸 후 부터는 AI에게 맡기면 정말 제가 예상한대로 잘 분리가 되었습니다. 그래서 그 뒤로 몇 시간 지나지 않아 basic과제를 마칠 수 있었습니다.

다시하면 더 잘해볼 수 있겠다, 아쉽다 하는 부분

리액트로 변환하는 과정은 사실상 AI 80%정도 라고 생각합니다. 일단 UI를 그리라고 시켰더니 거의 유사한 그림이 나왔고, 그 다음 테스트 코드를 복사해오라고 시켰더니 테스트를 리액트에 알맞게 가져왔고, 할인 및 계산 로직을 가져오라고 시켰더니 가져오고.. 이렇게 순차적으로 그저 시켰는데 정말 잘 가져왔습니다. 물론 과제 제출의 욕심이 있었기에 스스로 하기보다 처음부터 AI를 쓸 생각으로 진행했지만... basic부분을 하루라도 빨리 끝냈더라면 리액트로 변환하는 과정도 손수 진행해보고 싶은 마음이 들었습니다.. 그냥 마음속으로는 내가 basic을 잘 만들어놔서 AI가 쉽게 가져온걸꺼야.. 라고 생각하는 중이긴 합니다..!!

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)

이번 리팩토링 과제를 진행하며 궁금했던 부분은 과연 함수를 어느정도까지 쪼개야 하는 걸까? 하는 것입니다.

예시로 어떤 막대하고도 심오한 일을 하는 함수가 있는데, 이 함수를 한 네 가지 정도의 일처리로 분류를 할 수 있었다고 가정해보겠습니다. 그런데 그렇게 분류된 네 개의 일처리 함수 중 일부는 2~3가지로 또 분류 될 만한 함수였습니다.

그런식으로 최초의 큰덩어리에서 그 밑 작은 일 단위로 보면 네 가지, 그런데 그 각각의 함수를 또 분류하려고 보면 2~3가지

이렇게 최소 단위까지 분리한걸 결국 어떤 함수들은 조립자 역할만 하게 될것이고, 그 조립자들을 또 조립한 거대한 함수를 만들어야 최초에 막대하고 심오한 일을 할 수 있는 함수가 될텐데, 그렇다면 이 함수는 한 가지 일을 하는 함수가 맞는 건가요..?

어느정도까지 함수의 일처리 단위를 쪼개야하는지 아직 잘 감이 오지 않습니다..

과제 피드백

수고했어요! 전역변수를 해결하는 방식으로 만든 현대적 방법인 store를 떠올리고 변수를 직접 건드리는 형태가 아니라 액션을 통해서 격리하는 방식을 느껴본 부분이 참 좋네요. 또한 테스트가 제일 중요하다고 이 과제를 통해서 느꼈다고 하니 너무 좋습니다.

store을 만드는데 걸리는 시간이 거의 대부분이었다니 실제로 현업에서도 UI와 기능이 만들어지고 나면 그 다음에 수정하는 건 전부 데이터와 관련해서죠.

AI를 쓰지 않으니 몰입이 된다는 인사이트도 좋네요. 언젠가 우리 모두 AI를 쓰게 되더라도 그 몰입의 감각을 기억하면서 잘 쓰고 있는지 의존하고 있는지 구분하면 좋겠네요

(웃음) "... 그냥 마음속으로는 내가 basic을 잘 만들어놔서 AI가 쉽게 가져온걸꺼야.. 라고 생각하는 중이긴 합니다..!!" 라는건 실제입니다. 좋은 응집도와 분리하기 쉬운 구조를 가지고 만든다면 나머지는 정해진 방법이 있다는 뜻이고 그게 우리가 좋은 구조를 유지해야하는 이유인거죠! 잘했습니다. 만약 AI의 능력이 뛰어난 것이라면 내가 아무것도 하지 않아도 잘 했어야 겠죠ㅎ

함수를 어디까지 쪼개야 하는가? 정답은 있죠. 단일 책임이어야 한다는 거, 추상화 계층을 지켜야 한다는 거. 그렇지만 제가 알려준다고 한들 감은 본인이 잡아야 하는거겠죠? 감이 오지 않기에 최대한 다양한 방법들로 시도하면서 이정도가 좋구나 하는 것들을 이번 5주차 6주차 과제를 하면서 느껴보세요.

감을 찾는데 힌트를 느리자면 좋은 코드는 와! 너무 좋다 이런 느낌은 아니에요. 신경 쓰이지 않는 느낌이랍니다. 그래서 나쁜 코드가 아닌 코드들을에서 왜 나는 이 함수 정도면 괜찮다하고 넘어간거지? 하면서 그걸 말로 표현하거나 서술해보려고 해보세요. 경험적으로 감각은 길러지는데 왜 그런가에 대해서 언어로 정립을 해보려고 하면 훨씬 더 빨리 감을 잡을 수 있을거에요.

수고하셨습니다. 5주차 과제도 화이팅입니다.