지나가던 개발(zigae)

프론트엔드 클린코드 - 반복문 회피

2021년 8월 20일 • ☕️ 4 min read

clean code

for또는 while 반복문은 일반적으로 코드가 장황하고 오류가 발생하기 쉽기 때문에 퀄리티 높은 코드를 만들기 힘들다. 항상 length를 따로 관리해야 하고, 반복문 횟수가 조작 가능하다는 잠재적 위험도 포함하고 있다. 그리고 이와 같은 반복문은 코드를 반복 수행한다는 점을 제외하면 아무 의미를 가지지 않기 때문에 이해하는데 어려움이 발생한다.

반복문 대신 배열의 메소드 사용

최근 모던한 언어들은 반복문을 대신 할 방법이 있다. JS에서는 Array.map() 또는 Array.filter()등 과 같이 배열을 변환하고 반복하는 유용한 배열의 메소드가 많다.


Array.prototype.map() 이지만 작성 편의상 prototype 프로퍼티는 생략했다.


문자열 배열을 대문자로 반환해주는 toUppercase()를 예로 들어보자

const fruits = ['apple', 'banana', 'orange'];
for (let i = 0; i < fruits.length; i++) {
  fruits[i] = toUppercase(fruits[i]);
}

그리고 Array.map()을 사용해보자

const fruits = ['apple', 'banana', 'orange'];
const uppercaseFruits = fruits.map(name => toUppercase(name));

두 경우를 비교해보면 for 반복문 보다 배열의 메소드를 이용한 방법이 더 짧고 읽기 쉬워진 것을 알 수 있다.

배열 메소드의 선언적 의미

배열의 메소드가 왜 더 읽기 쉬울까? 단순히 짧아서가 아닌 각 메소드에는 명확한 의미가 담겨 있어서이다.

  • Array.map(): 동일한 수의 배열을 다른 배열로 변환
  • Array.find(): 배열에서 단일 요소를 검색하고 반환.
  • Array.some(): 일부 배열 요소에 만족하는 조건에 대한 상태 반환
  • Array.every(): 모든 배열 요소에 만족하는 조건에 대한 상태 반환

자세한 내용은 여기 참조

기존 반복문은 코드의 전체 내용을 읽기 전엔 코드가 수행하는 작업을 이해하기 어렵다. 우리는 항상 “무엇(what)“을 “어떻게(why)“에서 관심사를 분리 하고자 해야한다. 배열 메소드를 사용하면 콜백 함수로 전달하는 데이터만 신경 쓰면 되기 때문에 코드의 이해가 쉬워진다.

사이드이펙트 대응

사이드이펙트는 함수를 블랙박스로 취급할 수 없기 때문에 코드를 이해하기 어렵게 만든다. 사이드이펙트가 있는 함수는 입출력이 동일하지 않을 뿐더러 예측할 수 없는 방식으로 우리의 코드에 영향을 미친다.

이전에 언급된 배열의 메소드를 제외하고, Array.forEach()는 사이드이펙트가 없고 반환 값만 사용한다는 것을 의미한다. 하지만 사이드이펙트가 발생하면 개발자는 사이드이펙트를 생각하지 않기 때문에 코드를 잘못 읽을 수 있다.

아래 예제를 살펴보자

errors.forEach(error => {
  console.error(error);
});

에러가 발생한다면 반복을 멈출 필요가 있다. 하지만 Array.forEach()는 반복을 멈출 수 없다. 그렇기에 for of 사용을 권장한다.

for (const error of errors) {
  console.error(error);
}

일반 for루프와 달리 반복 횟수를 조작할 수 없기 때문에 모든 배열 요소에 대한 반복의 명확한 의미를 갖는다. 그리고 우리는 break를 통해 루프를 중단할 수 있다.

반복문이 더 나을 수도 있다.

배열의 메소드가 항상 반복문 보다 나은 것은 아니다. 예를 들면 Array.reduce()의 경우엔 종종 일반 반복문 보다 코드의 복잡도를 높인다.

아래 반복문을 살펴보자.

const tableData = [];
if (props.item && props.item.details) {
  for (const client of props.item.details.clients) {
    for (const config of client.errorConfigurations) {
      tableData.push({
        errorMessage: config.error.message,
        errorLevel: config.error.level,
        usedIn: client.client.name
      });
    }
  }
}

반복문을 피하기 위해 Array.reduce()를 이용해 작성해보자

const tableData =
  props.item &&
  props.item.details &&
  props.item.details.clients.reduce(
    (acc, client) => [
      ...acc,
      ...client.errorConfigurations.reduce(
        (inner, config) => [
          ...inner,
          {
            errorMessage: config.error.message,
            errorLevel: config.error.level,
            usedIn: client.client.name
          }
        ],
        []
      )
    ],
    []
  );

정말 읽기 쉬운 코드인가? 그렇지 않다. 적어도 필자는 이중포문이 더 간단해 보인다.

사실 Array.map() 을 이용하면 조금 더 간단하게 가능하긴 하다

결론

  • 반복문을 베열의 메소드로 대체
  • 함수의 사이드이펙트 방지

참조