잘 만든 함수는 조립이 가능한 레고 블록이다

jinew·2025년 1월 16일

🍎 Javascript

목록 보기
15/22
post-thumbnail

코드를 작성하고 피드백을 받다 보면 듣는 말들이 있을 거다. "이 함수는 기능이 명확하지 않다", "하나의 함수에 두 가지 이상의 기능이 있다", "재사용성이 없어 보인다", "중복된 내용이 많아서 함수로 만들 수 있겠다" 등등 개발 입문자라면 수도 없이 들어 본 피드백이겠지만, 사실 '잘 만든 함수'에 대한 명확한 감이 없기도 하다. 그래서 오늘은 재사용 가능한 함수에 대해 알아보려 한다.



💬 모두가 재사용 가능한 함수를 계속 언급하는 이유


: 함수는 프로그래밍의 핵심 도구다. 잘 만들어진 함수는 마치 조립이 가능한 레고 블록과 같다. 레고 블록은 크기와 모양이 다르지만, 어디서든 서로 맞물릴 수 있도록 설계되어 있다. 함수도 마찬가지로 잘 설계된 함수는 다양한 상황에서 재사용이 가능하고 서로 다른 코드와 쉽게 결합될 수 있다.

function calculateArea(width, height) {
  return width * height;
}

// 다른 곳에서 활용
const rectangleArea = calculateArea(5, 10); // 50
const squareArea = calculateArea(7, 7);     // 49
  • calculateArea 함수는 도형의 너비를 구하는 공식을 구현했다. 따라서 직사각형인 rectangleArea에서도, 정사각형인 squareArea에서도 같은 로직을 적용해 재사용이 가능하다.

❔ 왜 재사용 가능한 함수가 중요할까?

  1. 코드의 중복 감소 : 동일한 코드를 여러 번 작성할 필요가 없다.
  2. 유지보수의 용이성 : 함수가 한 곳에서 관리되므로 수정이 간편하다.
  3. 가독성 향상 : 코드의 구조가 명확해지고, 의도가 잘 드러난다.
  4. 생산성 증가 : 이미 만들어진 함수를 재활용함으로써 시간을 절약할 수 있다.

이제 재사용성이 있는 함수의 중요성을 알아쓰니, 재사용 가능한 함수의 특징을 알아보고 적용해보자!



🧽 재사용 가능한 함수의 특징


1. 단일 책임 원칙 (Single Responsibility Principle)

: 함수는 하나의 작업만 수행해야 한다❗❗❗ 좋은 코드에 대한 글을 읽어보면 늘 나오는 말이다. 함수가 하나의 명확한 기능만을 담당할 때 가독성이 좋아지고, 다른 코드와의 결합이 쉬워진다. 종이를 일자로 자르기 위해 가위를 샀는데 알고보니 단순히 자르기만 하는 게 아니라 물결로 잘라지기까지 하는 핑킹가위를 사버렸다면 우리가 원하던 가위가 아니게 된다.
이런 것처럼 각 함수가 하나의 기능으로만 작동할 수 있도록 구조를 잡는 것이 좋다.

// 🚫 잘못된 예: 여러 작업을 수행하는 함수
function processUserData(user) {
  console.log(user.name);
  user.isActive = true;
  return `${user.name} is now active`;
}

// ✅ 개선된 예: 한 가지 작업만 수행
function logUserName(user) {
  console.log(user.name);
}

function activateUser(user) {
  user.isActive = true;
  return `${user.name} is now active`;
}
  • processUserDate 함수를 살펴보면, user.name으로 사용자의 이름을 로그에 찍어주는 기능과 특정 유저가 활동중인지를 판별하는 두 가지의 기능을 담당한다.
  • 이 함수를 각각 logUserNameactivateUser로 분리해 사용자의 이름을 로그에 찍는 기능과 사용자의 활동을 확인하는 기능을 명확히 할 수 있게 됐다. 확실히 기존 함수명과 비교해봤을 때도 가독성이 좋아졌다.
  • 이렇게 함수에 한 가지 기능만을 할당하고 그 기능을 쉽게 이해할 수 있도록 함수명을 지어주면 더할 나위가 없다!

2. 입력과 출력의 명확성

: 함수는 명확한 입력 (인자 / input)과 예측 가능한 출력 (반환값 / output)을 가져야 한다. 함수의 동작을 쉽게 이해하고 테스트하기 위함이다.

// 비효율적인 함수: 출력이 명확하지 않음
let multiplier = 2;

function multiply(num) {
  return num * multiplier;
}

// 개선된 함수: 입력과 출력이 명확함
function multiply(num, factor) {
  return num * factor;
}

console.log(multiply(5, 2)); // 10
  • num을 기준이 되는 특정 숫자와 곱해주는 함수 multiply가 있다고 했을 때, 첫 번째 함수는 multiplier라는 외부 상태값에 의존하기 때문에 비효율적이다. 만약 multiplier의 선언과 할당 구문이 코드의 맨 꼭대기에 있고, multiply 함수가 모듈화되어 아예 다른 페이지로 분리되어있다거나 n 백줄 짜리 코드 맨 밑에 작성되어 있다고 한다면 num에 곱해지는 multiplier가 무엇인지 한참을 찾아야 할 것이고, 이 값이 곱해졌을 때 뭐가 나올지 조차 예상이 어렵다.

  • 혹은 고정으로 곱해져야하는 인자가 2가 아니라 3인 경우가 필요하다면 이 함수를 재사용할 수 없게 된다. (multiplier에 할당된 값을 3으로 바꾸면 되지 않느냐라고 할 수 있지만, 그렇게 되면 이미 해당 함수를 사용하고 있던 기존 값들도 모두 변경되어 원하지 않는 방향의 수정이 발생할 수 있다.)

  • 하지만 아래 새로 작성된 함수는 입력값과 출력값이 명확하다. 함수에 할당되는 input인 인자값끼리 곱한 값을 반환하기 때문에 외부 상태에 의존하지 않는다. 따라서 고정으로 곱해져야 하는 factor의 값이 이 함수를 사용할 때마다 변동되어야 한다고 해도 그에 맞게 값을 할당할 수 있어 재사용성이 높아진다.


3. 의존성 최소화

: 함수는 글로벌 변수와 같은 외부 상태에 의존하지 않고 순수 함수로 작성되는 것이 좋다.

// 외부 상태에 의존하는 함수
let taxRate = 0.1;
function calculateTotal(price) {
  return price + price * taxRate;
}

// 순수 함수로 개선
function calculateTotal(price, taxRate) {
  return price + price * taxRate;
}

console.log(calculateTotal(100, 0.1)); // 110
  • 위의 함수는 외부 상태값인 taxRate에 의존한다. 따라서 이 함수를 사용하기 위해서는 반드시 taxRate의 값을 추적해야 할 것이고, 만일 환율의 변동이 있더라도 기존 값을 고려한 수정이 필요할 것이다. 그리고 환율이 다른 나라에서는 사용할 수가 없다 😅

  • 개선된 함수는 taxRate를 매개변수로 받아 동일한 입력값에 대해 항상 동일한 출력값을 반환한다. 이는 테스트와 디버깅을 용이하게 하며 재사용성을 높여준다.



💌 재사용 가능한 함수 작성 Tip!


1. 기본값 활용

: 인자의 기본값을 설정하면 함수 호출 시 일부 인자를 생략할 수 있어 유연성이 높아진다.
예를 들어 사용자의 이름을 인자로 받아 "Hello, name!" 으로 출력하는 함수가 있다고 할 때, 우리는 이름을 입력받지 못한 경우를 고려해야 할 것이다. 그럼 아래와 같이 작성한다.

function greet(name) {
  if (name === "" ) name = "Guest"
  return `Hello, ${name}!`;
}

console.log(greet()); // Hello, Guest!
console.log(greet('Alice')); // Hello, Alice!
  • 인자로 name을 전달받으나 만약 그 값이 빈 문자열일 경우 "Guest"로 대체하도록 한다. 이 조건문이 군더더기없어 보인다면 아래 스니펫을 꼭 읽어보자 😄😃😁

function greet(name = 'Guest') {
  return `Hello, ${name}!`;
}

console.log(greet()); // Hello, Guest!
console.log(greet('Alice')); // Hello, Alice!
  • 인풋값으로 받을 파라미터에 name = "Guest"로 작성해 기본값을 할당한다. 이러면 불필요한 조건문을 작성할 필요도 없고, 함수를 호출할 때 name 인자가 생략되어도 오류가 나지 않아 유연성이 높아진다!

2. 고차 함수 사용

: 고차 함수는 다른 함수를 인자로 받거나, 함수를 반환하는 함수를 말한다. 이를 사용하면 함수의 재사용성을 극대화 할 수 있다.

function createMultiplier(factor) {
  return function (num) {
    return num * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15
  • double 함수는 createMultiplier 함수의 인자 factor2를 전달해 num에 2를 곱한 값을 반환한다. 따라서 double(5)factor2num 5를 곱한 값인 10이 반환된다.

  • 마찬가지로 triple 함수는 createMultiplier 함수의 인자 factor3을 전달해 num에 3을 곱한 값을 반환한다. 그리하여 triple(5)15가 반환된다.

  • doubletriple 함수의 로직을 따로 구현할 필요가 없는 것은 바로 고차 함수를 사용해 재사용성을 확장했기 때문이다!


3. 함수의 이름을 명확히 ⭐

: 함수의 이름은 그 함수의 역할을 명확히 설명해야 한다. 이름만 보고도 함수가 무엇을 하는지 알 수 있어야 한다!

// 불명확한 이름
function doStuff(a, b) {
  return a + b;
}

// 명확한 이름
function addNumbers(a, b) {
  return a + b;
}
  • doStuff 라는 함수명을 봤을 때 어떤 생각이 들었냐면 .. '뭔소리야' 였다. 그 이후로는 return문에서 반환하는 값을 보고나서야 이 함수의 역할을 알 수 있었다.

  • 하지만 이 함수의 이름을 addNumbers 로 수정하고 나서는, 굳이 return문을 해석하지 않아도 '아 이 함수는 인자 ab를 더한 값을 반환하겠구나' 라고 쉽게 예상할 수 있다.




모두가 강조하는 '재사용 가능한 함수'에 대해 공부해봤다!
앞으로 함수를 설계할 때는 1) 함수가 여러 개의 기능을 담당하지는 않는지, 2) 입력과 출력이 명확한지, 3) 의존성을 최소화해 재사용성을 고려했는지를 염두에 두고 만들어야겠다.
마지막으로는 4) 함수의 이름이 그 기능을 잘 설명하고 있는지 까지도!

profile
멈추지만 않으면 도착해 🛫

0개의 댓글