프론트엔드/JavaScript

[JavaScript] 클린 코드 - 2. 함수 (1)

1. 함수 인자는 2개 이하가 이상적이다.

매개변수의 개수가 적다는 것은 테스트할 경우의 수가 줄어든다는 것을 의미한다.

1~2개가 이상적이다. 인자가 2개가 넘으면 그 함수는 너무 많은 역할을 하고 있는 중이다.

참고) ES6의 비구조화(destructuring) 문법을 사용한다면 함수의 속성을 명확하게 보여줄 수 있다.

// BAD
function createMenu(title, body, buttonText, cancellable) {
  // ...
}

// GOOD! - 비구조화(destructuring)
function createMenu({ title, body, buttonText, cancellable }) {
  // ...
}

createMenu({
  title: "Foo",
  body: "Bar",
  buttonText: "Baz",
  cancellable: true,
});

 

2. 함수는 하나의 행동만 해야 한다.

이는 소프트웨어 공학에서도 가장 중요한 규칙 중 하나이기도 하다!

함수가 하나의 행동만 하게 설계한다면 여러 장점을 가질 수 있다.

  • 테스트하기 쉬워진다.
  • 코드를 이해하기 쉬워진다.
  • 유지보수가 쉬워진다.
// BAD - 2개 기능 가짐
function emailClients(clients) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}

// GOOD! 한 기능씩 분리
function emailClients(clients) {
  clients.filter(isClientActive).forEach(email);
}

function isClientActive(client) {
  const clientRecord = database.lookup(client);
  return clientRecord.isActive();
}

 

3. 함수명은 함수가 무슨 일을 하는지 알 수 있게 짓는다.

// BAD - 뭘 Add하는 함수?
function AddToDate(date, month) {
  // ...
}

const date = new Date();

AddToDate(date, 1);

// GOOD!
function AddMonthToDate(date, month) {
  // ...
}

const date = new Date();
AddMonthToDate(date, 1);

 

4. 함수명은 하나의 의미만 갖는다.

만약 함수명이 여러 의미를 내포하고 있다면 그 함수는 여러 기능을 가졌다는 의미이기도 하다.

이런 함수들을 하나씩 분리한다면 재사용성을 높이고 테스트하기 쉬워진다.

// BAD
function parseBetterJSAlternative(code) {
  const REGEXES = [
    // ...
  ];

  const statements = code.split(" ");
  const tokens = [];
  REGEXES.forEach((REGEX) => {
    statements.forEach((statement) => {
      // ...
    });
  });

  const ast = [];
  tokens.forEach((token) => {
    // lex...
  });

  ast.forEach((node) => {
    // parse...
  });
}

// GOOD!
function tokenize(code) {
  const REGEXES = [
    // ...
  ];

  const statements = code.split(" ");
  const tokens = [];
  REGEXES.forEach((REGEX) => {
    statements.forEach((statement) => {
      tokens.push(/* ... */);
    });
  });

  return tokens;
}

function lexer(tokens) {
  const ast = [];
  tokens.forEach((token) => {
    ast.push(/* ... */);
  });

  return ast;
}

function parseBetterJSAlternative(code) {
  const tokens = tokenize(code);
  const ast = lexer(tokens);
  ast.forEach((node) => {
    // parse...
  });
}

 

5. 중복된 코드를 작성하지 않는다.

중복된 코드를 가진다는 것은 수정할 때 여러 곳을 고쳐야 한다는 의미이기도 하다.

흔히 사소한 차이를 어떻게 다룰지 난감해서 중복된 코드를 작성할 때가 많다.

이럴땐 함수, 모듈, 클래스 등을 이용해 추상화 하는 것이 좋다.

 

하지만 잘못된 추상화는 차라리 중복된 코드만도 못할 수 있다.

공통된 부분을 잘 구분하여 추상화 해야 한다. 그래서 수정이 발생해도 다른 모든 코드에 정상 반영되도록 하자!

// BAD - 두 함수가 유사하다
function showDeveloperList(developers) {
  developers.forEach((developers) => {
    const expectedSalary = developer.calculateExpectedSalary();
    const experience = developer.getExperience();
    const githubLink = developer.getGithubLink();
    const data = {
      expectedSalary,
      experience,
      githubLink,
    };

    render(data);
  });
}

function showManagerList(managers) {
  managers.forEach((manager) => {
    const expectedSalary = manager.calculateExpectedSalary();
    const experience = manager.getExperience();
    const portfolio = manager.getMBAProjects();
    const data = {
      expectedSalary,
      experience,
      portfolio,
    };

    render(data);
  });
}

// GOOD!
function showEmployeeList(employees) {
  employees.forEach((employee) => {
    const expectedSalary = employee.calculateExpectedSalary();
    const experience = employee.getExperience();

    let portfolio = employee.getGithubLink();

    if (employee.type === "manager") {
      portfolio = employee.getMBAProjects();
    }

    const data = {
      expectedSalary,
      experience,
      portfolio,
    };

    render(data);
  });
}

 

6. Object.assign을 사용해 기본 객체를 만들자.

  • Object.assign() - 하나 이상의 객체로부터 대상 객체로 속성을 복사한 뒤 대상 객체를 반환한다.
// BAD
const menuConfig = {
  title: null,
  body: "Bar",
  buttonText: null,
  cancellable: true,
};

function createMenu(config) {
  config.title = config.title || "Foo";
  config.body = config.body || "Bar";
  config.buttonText = config.buttonText || "Baz";
  config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
}

createMenu(menuConfig);

// GOOD!
const menuConfig = {
  title: "Order",
  // 유저가 'body' key의 value를 정하지 않았다.
  buttonText: "Send",
  cancellable: true,
};

function createMenu(config) {
  // Object.assign을 통해 객체를 기본값으로 초기화 
  config = Object.assign(
    {
      title: "Foo",
      body: "Bar",
      buttonText: "Baz",
      cancellable: true,
    },
    config
  );
  // config = {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
}

createMenu(menuConfig);

 

7. 매개변수로 플래그를 사용하지 말자

  • 플래그(flag) - 상태를 제어하는 용도의 변수

플래그를 사용한다는 자체가 그 함수가 한 가지 이상의 역할을 하고 있다는 것을 의미한다.

만약 플래그 변수의 상태에 따라 함수의 실행이 나뉜다면 함수를 분리하자.

// temp 상태에 따라 함수 기능이 나뉜다
function createFile(name, temp) {
  if (temp) {
    fs.create(`./temp/${name}`);
  } else {
    fs.create(name);
  }
}

// GOOD!
function createFile(name) {
  fs.create(name);
}

function createTempFile(name) {
  createFile(`./temp/${name}`);
}

 

8. 사이드 이펙트(Side Effect)를 피한다.

흔히 사이드 이펙트를 '부작용'으로 알고 있지만 프로그래밍에선 '부수효과'의 의미에 가깝다.

JavaScript 관점에서 사이드 이펙트는 한마디로 '코드가 외부 세계에 영향을 주거나 받는 행위'이다.

여기서 외부란 네트워크 통신뿐 아니라 코드의 외부 스코프, 사용자의 액션 등도 될 수 있다.  

어떤 함수가 일관된 결과를 보장하지 않거나 함수 외부에 영향을 준다면 사이드 이펙트로 생각하면 좋다.

 

8-1. 같은 사이드 이펙트를 만드는 함수가 여러 개 존재하면 안 된다.

// BAD
let name = "Ryan McDermott";

// 전역변수 name은 이 함수로 인해 배열이 된다. 만일 다른 함수에서 이 전역변수에 접근한다면.. 오류가 날 것이다.
function splitIntoFirstAndLastName() {
  name = name.split(" ");
}

splitIntoFirstAndLastName();

console.log(name); // ['Ryan', 'McDermott'];

// GOOD!
function splitIntoFirstAndLastName(name) {
  return name.split(" ");
}

const name = "Ryan McDermott";
const newName = splitIntoFirstAndLastName(name);

console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];

 

8-2. 불변 객체 (Immutable Object)를 사용한다.

만약 객체에 추가, 수정, 삭제 이벤트가 발생할 때 통신 문제로 인해 원본이 의도치 않게 변경되면 어떨까?

이 객체를 참조하는 다른 함수가 원본의 변경에 영향을 받게 되는 참사가 일어날 것이다.

이 때 불변 객체를 사용하여 원본을 유지하며 값이 변경될 땐 복제된 새로운 배열을 반환하면 좋다.

  • 불변 객체 - 생성 후 그 상태를 바꿀 수 없는 객체 ( ↔ 가변 객체)

참고) immutable.js 등의 라이브러리는 불변함을 유지하며 상태관리 하는 것을 도와준다.

// BAD
const addItemToCart = (cart, item) => {
  cart.push({ item, date: Date.now() });
};

// GOOD! - 불변객체 (Immutable Object)
const addItemToCart = (cart, item) => {
  // [...arr1, ...arr2] = arr1.concat(arr2) - 배열을 끝에 이어붙인다.
  return [...cart, { item, date: Date.now() }];
};

 

9. 전역 함수를 사용하지 않는다.

우리가 그냥 파일에 함수를 선언하면 이 함수는 전역 객체(ex: window)에 바인딩된다.

이렇게 전역 환경을 사용하는 것은 좋지 않다. 같은 기능을 가진 라이브러리와 충돌이 날 수 있다.

대신 ES6의 class를 통해 전역 객체를 상속하는 방법을 더 추천한다. 

// BAD - 기존 라이브러리와 충돌 위험
Array.prototype.diff = function diff(comparisonArray) {
  const hash = new Set(comparisonArray);
  return this.filter((elem) => !hash.has(elem));
};

// GOOD! - 전역 객체 Array를 상속하여 구현
class SuperArray extends Array {
  diff(comparisonArray) {
    const hash = new Set(comparisonArray);
    return this.filter((elem) => !hash.has(elem));
  }
}

 

참조