KEYUM 프로젝트 리팩토링 [~ing] 🛠

HyeonE·2023년 5월 22일
1
post-thumbnail

본 글은 KEYUM 프로젝트 코드를 리팩토링 한 뒤 작성한 글입니다 ✅

리팩토링? WHY? 💡

최근 벨로그에서 읽은 IT 이슈 글 중에서 가장 인상깊은 내용을 소개해드리려 합니다. 그 글에서는 코드 리팩토링에 대한 질문인 "코드는 언제 리팩토링해야 하나요?"에 대해 답변하는데, "코드에서 악취가 나면 시작해라."라는 문구가 인상깊었습니다.

이 문구는 코드를 읽을 때마다 개선 가능한 부분이 보이지만, 말로만 하고 실제로 개선하지 않는 상황인 저에게 큰 자극을 부여해주었습니다. 코드가 향기로운 꽃이 아닌, 악취를 풍기는 것처럼 느껴진다면, 그것은 리팩토링을 시작해야 할 시점이라는 것을 알려준 것 같습니다.

이제 저는 더 이상 말로만 하지 않고 실행으로 옮겨보려고 합니다. 코드를 클린하고 유지보수하기 좋은 형태로 만들기 위해 노력하고자합니다.

리팩토링은 어떻게 진행하면 될까? 😓

리팩토링을 진행하는 방법에 있어서는 정말 수많은 방법이 있을 것이라고 생각됩니다. 저는 많은 방법 중 가장 대표적인 케이스로 진행해볼려고 합니다.

  1. 코드 베이스 분석
    우선, 전체 코드 베이스를 분석하여 어떤 부분에서 리팩토링이 필요한지 파악하였습니다. 중복된 코드, 긴 메서드, 복잡한 조건문 등을 식별하고 개선할 대상을 목록화했습니다.

  2. 모듈화와 기능 단위 분리
    기능 단위로 코드를 분리하여 모듈화하고, 각 모듈을 독립적으로 개선할 수 있도록 구조를 재조정했습니다. 이렇게 함으로써 코드의 의존성을 최소화하고, 테스트와 디버깅을 쉽게 할 수 있게 되었습니다.

  3. 중복 코드 제거
    중복된 코드를 찾아 공통 모듈로 분리하였습니다. 이를 통해 코드의 길이를 줄이고 유지보수성을 높일 수 있었습니다.

  4. 가독성 개선
    가독성이 떨어지는 부분을 발견하고, 의미 있는 변수명과 주석을 추가하여 코드를 명확하게 만들었습니다. 또한, 들여쓰기와 코드 형식을 일관되게 정리하여 코드를 더 쉽게 이해하고 수정할 수 있도록 하였습니다.

  5. 성능 최적화
    성능 저하가 발생하는 부분을 찾아 개선하였습니다. 알고리즘 최적화, 자원 관리 등을 통해 프로젝트의 성능을 향상시킬 수 있었습니다.

개선된 코드 💻

수많은 코드의 리팩토링이 진행되고 있지만 이 중에서 제가 꼭 기억하고 싶은 코드들에 대해서는 이 공간에 기록을 남길려고합니다.

# 1 (루틴 요일 선택 페이지)

<이전>

  // 요일 선택 기능 구현
  const onSelect = useCallback(
    (id, title) => {
      const newSelected = new Map(selected);
      newSelected.set(id, !selected.get(id));
      setSelected(newSelected);
      !selected.get(id)
        ? setDayText([...dayText, title])
        : setDayText(dayText.filter(str => str !== title));
    },
    [selected],
  );

<리팩토링 후>

  // 요일 선택 기능 구현
  const handleSelect = useCallback(
    (id, title) => {
      setSelected(selected => {
        const newSelected = new Map(selected);
        newSelected.set(id, !selected.get(id));
        !selected.get(id) ? dayText.push(title) : dayText.pop();
        return newSelected;
      });
    },
    [dayText],
  );

기존 코드에서는 setSelectedsetDayText를 동일한 useCallback 내에서 사용하고 있었습니다. 따라서 의존성 배열에 selecteddayText를 동시에 넣어야 했습니다. 하지만 개선된 코드에서는 dayText의 변경에만 의존하므로 dayText만을 의존성 배열에 포함시켰습니다.

개선된 코드에서는 dayText를 변경하는 부분에 조건문을 사용하였습니다. 만약 선택 상태가 true라면 이미 존재하는 title을 dayText에서 제거하고, false라면 title을 dayText에 추가합니다. 이렇게 함으로써 배열의 중복된 값을 추가하거나 제거하는 로직이 보다 명확하게 표현되었습니다.

변경된 dayText 값을 업데이트하기 위해 이전 dayText 배열을 활용하여 새로운 배열을 생성하였습니다. 이를 통해 이전 값의 불변성을 유지하고 새로운 배열을 생성하여 상태를 업데이트합니다.

이러한 개선을 통해 코드의 가독성과 유지보수성이 향상되었으며, 기능적인 측면에서도 동일한 동작을 수행합니다.

# 2 (API)

<이전>

if (status === 403) {
  // 처리 로직
}

<리팩토링 후>

if (status === 403 && originalRequest.url !== '/auth/refresh') {
  // 처리 로직
}

콘솔을 찍어보기 전에는 몰랐지만, 찍은 뒤로는 계속 초기 온보딩 화면에서 토큰 재발급 API가 무한루프를 돌고있었습니다.. (정말 심각한 문제..)

이 코드 변경은 조건문의 추가 조건으로 originalRequest.url !== '/auth/refresh'를 포함시킨 것입니다. 이로 인해 상태 status가 403일 때만 처리되는 것이 아니라, 동시에 originalRequest.url이 '/auth/refresh'와 다를 경우에만 처리되도록 변경되었습니다.

이 변경은 특정 요청인 경우에만 처리하고자 할 때 유용합니다. 이전 코드에서는 모든 status 403 상태에 대해 처리되었지만, 개선된 코드에서는 '/auth/refresh' 요청에 대해서는 해당 처리를 제외하고 처리됩니다. 이를 통해 상황에 맞는 세분화된 처리를 할 수 있게 되었습니다.

# 3 (progress 페이지)

<이전>

  // 날짜 선택시 루틴리스트 생성
  const setRoutinesByDate = () => {
    let tmp = [];
    for (var i = 0; i < selectTodo.length - 1; i++) {
      if (selectTodo[i].day === dateSelected) {
        let tmpSelected = JSON.parse(JSON.stringify(selectTodo[i]));
        if (
          selectTodo[i].routineCategory == '일상' &&
          selectTodo[i].completed == true
        ) {
          tmpSelected.categoryImg = lifeGray;
        } else if (
          selectTodo[i].routineCategory == '일상' &&
          selectTodo[i].completed == false
        ) {
          tmpSelected.categoryImg = life;
        } else if (
          selectTodo[i].routineCategory == '학습' &&
          selectTodo[i].completed == true
        ) {
          tmpSelected.categoryImg = educationGray;
        } else if (
          selectTodo[i].routineCategory == '학습' &&
          selectTodo[i].completed == false
        ) {
          tmpSelected.categoryImg = education;
        } else if (
          selectTodo[i].routineCategory == '마음관리' &&
          selectTodo[i].completed == true
        ) {
          tmpSelected.categoryImg = mentalGray;
        } else if (
          selectTodo[i].routineCategory == '마음관리' &&
          selectTodo[i].completed == false
        ) {
          tmpSelected.categoryImg = mental;
        } else if (
          selectTodo[i].routineCategory == '운동' &&
          selectTodo[i].completed == true
        ) {
          tmpSelected.categoryImg = healthGray;
        } else if (
          selectTodo[i].routineCategory == '운동' &&
          selectTodo[i].completed == false
        ) {
          tmpSelected.categoryImg = health;
        }
        tmp.push(tmpSelected);
      }
    }
    setRoutines(tmp);
  };

<리팩토링 이후>

  // 날짜 선택시 루틴리스트 생성
  const setRoutinesByDate = () => {
    const tmp = selectTodo
      .filter(item => item.day === dateSelected)
      .map(item => {
        const tmpSelected = JSON.parse(JSON.stringify(item));
        tmpSelected.categoryImg =
          categoryImgs[item.routineCategory][item.completed];
        return tmpSelected;
      });
    setRoutines(tmp);
  };

개선된 코드에서는 말도 안되는 for 루프 대신 filtermap 함수를 사용하여 더 간결하고 가독성이 좋은 방식으로 변경하였습니다.

filter 함수는 selectTodo 배열에서 item.daydateSelected와 일치하는 요소들만 필터링하여 새로운 배열을 생성합니다.

map 함수는 필터링된 배열의 각 요소에 대해 새로운 객체를 생성하고 categoryImg 값을 설정합니다. 이때, categoryImgs 객체를 활용하여 routineCategorycompleted 값에 따라 적절한 이미지를 선택합니다.

최종적으로 생성된 배열을 setRoutines 함수를 사용하여 업데이트합니다.

이 개선된 코드는 루틴 리스트 생성 로직을 간결하고 효율적으로 작성한 것으로, 가독성과 유지보수성을 향상시킬 수 있습니다.

# 4 (메인 페이지)

<이전>

  // 루틴 전체 불러오기
  const setTodo = async res => {
    res = selectTodo[selectTodo.length - 1];
    let completedTmp, totalTmp;
    completedTmp = [0, 0, 0, 0];
    totalTmp = [0, 0, 0, 0];
    for (var i = 0; i < res.length; i++) {
      // 루틴 전체 불러오기
      if (res[i].routineCategory === '일상') {
        if (res[i].completed === true) {
          completedTmp[0] += 1;
          totalTmp[0] += 1;
        } else {
          totalTmp[0] += 1;
        }
      } else if (res[i].routineCategory === '학습') {
        if (res[i].completed === true) {
          completedTmp[1] += 1;
          totalTmp[1] += 1;
        } else {
          totalTmp[1] += 1;
        }
      } else if (res[i].routineCategory === '마음관리') {
        if (res[i].completed === true) {
          completedTmp[2] += 1;
          totalTmp[2] += 1;
        } else {
          totalTmp[2] += 1;
        }
      } else if (res[i].routineCategory === '운동') {
        if (res[i].completed === true) {
          completedTmp[3] += 1;
          totalTmp[3] += 1;
        } else {
          totalTmp[3] += 1;
        }
      }
      setTodoCompleted(completedTmp);
      setTodoTotal(totalTmp);
    }
  };

<리팩토링 이후>

  // 루틴 전체 불러오기
  const setTodo = async res => {
    res = selectTodo[selectTodo.length - 1];
    const completedTmp = [0, 0, 0, 0];
    const totalTmp = [0, 0, 0, 0];

    for (let i = 0; i < res.length; i++) {
      const category = res[i].routineCategory;
      // index = category 값에 대응하는 completedTmp와 totalTmp의 index
      const index = {일상: 0, 학습: 1, 마음관리: 2, 운동: 3}[category];
      if (res[i].completed === true) {
        completedTmp[index] += 1;
      }
      totalTmp[index] += 1;
    }

    setTodoCompleted(completedTmp);
    setTodoTotal(totalTmp);
  };

개선된 코드에서는 completedTmptotalTmpconst로 선언하였습니다.

루틴의 카테고리(routineCategory)에 따라 index 값을 할당하는 객체 리터럴을 사용하여 인덱스를 동적으로 할당하였습니다. 이렇게 함으로써 if-else 문을 사용하는 대신 객체의 속성을 조회하여 해당하는 인덱스에 접근할 수 있습니다.

루틴이 완료된 경우 해당 카테고리의 completedTmp 값을 증가시키고, totalTmp 값을 증가시킵니다.

최종적으로 completedTmp와 totalTmp를 업데이트합니다.

이 개선된 코드는 반복문 내에서 객체 리터럴을 사용하여 조건을 처리하고, 중복되는 코드를 줄여 가독성을 향상시킨 것으로, 유지보수성과 가독성을 개선하였습니다.

리팩토링은 계속 진행중... 🙌

키움 프로젝트의 리팩토링을 통해 코드의 효율성과 유지보수성을 높일 수 있었습니다. 이를 통해 개발과 유지보수 단계에서 더욱 효율적으로 작업할 수 있게 되었습니다. 리팩토링은 프로젝트의 지속적인 성장과 개선을 위한 중요한 활동이라고 생각됩니다.

저는 앞으로도 지속적인 코드 개선과 리팩토링을 통해 프로젝트를 발전시켜 나갈 계획입니다!! 😀😀

profile
프론트엔드 개발자가 되고싶은 대학생

0개의 댓글