본문 바로가기
안정적인 프로덕트를 위한 고민

순수함수와 부수효과를 나누어 개발하는게 좋을까?

by MINTOEngineer 2023. 12. 16.

이 글은 프리온보딩 아키텍쳐 강의를 듣고 현업에서 적용해보면서 느꼈던 고민들을 정리한 글 입니다.

 

 

글을 시작하기 앞서 순수함수와 부수효과가 있는 함수란 무엇일까?

 

순수함수

 

순수함수는 외부의 상태를 변경하지 않으면서 동일한 인자에 대해 항상 똑같은 값을 리턴하는 함수입니다.

 

라고 하는데 리액트를 조금 해보신 분이라면 저희가 사용하는 상태변경 함수를 호출하지 않는 함수라는 의미로 이해가 될꺼고 뒤에 나오는 동일한 인자에 대해 항상 똑같은 값을 리턴한다는 말이 조금 헤깔릴 수도 있을꺼 같습니다. 

 

function add(a:number, b:number):number {
	return a + b;
}

 

코드를 보면 이해할 수 있는데 인자에 어떤값이 들어가던지 항상 동일하게 두 값을 더한 값이 나오는걸 의미한다고 볼 수 있습니다.

말 그대로 순수함수란 외부에 아무런 영향을 주지 않고 주어진 역할만 수행후 그에대한 결과물만 리턴하는 함수라고 볼 수 있고 이런 함수는 개발자하여금 보고 바로 어떤 역할을 하는 함수인지 이해할 수 있습니다. 그리고 함수 내부에 로직이 의도적으로 동작하는지 테스트 코드도 작성할 수 있는 좋은 코드라고 볼 수 있습니다.

 

하지만 순수함수만을 가지고는 어플리케이션을 개발할 수는 없습니다. 순수함수의 결과물을 가지고 외부 상태에 특정한 액션을 줘야지만 서비스가 동작하기 때문에 외부의 액션도 고려를 해야합니다. 

 

부수 효과 함수 (액션 함수)

리액트에서 부수 효과가 있는 액션 함수는 대표적으로 setState와 useState를 예로 들 수 있습니다. 이 함수들이 호출되면 전에 존재했던 상태값과 주소값을 비교후 달라졌으면 컴포넌트를 리렌더링 하는 함수입니다. 

const [count, setCount] = useState(0);

const handleClickCount = () => {
	setCount(count + 1);
}

return (
	<div onClick={handleClickCount}>{count}</div>
}

 

위에 리액트 컴포넌트 코드도 클릭 이벤트가 발생하면 특정한 액션을 주는 setCount가 호출되어 컴포넌트가 리렌더링이 발생합니다. 

 

그럼 순수 함수와 액션 함수를 분리하는 이유는 뭘까?

일단 코드부터 살펴볼까요?

type Email = string;

const validateAndSendEmail = async (email: Email, content: string) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (emailRegex.test(email)) {
    // 이메일 유효성 검사 통과 후, 이메일 전송 로직
    console.log(`Sending email to ${email}: ${content}`);
    // 예시를 위한 가상의 비동기 처리
    await new Promise(resolve => setTimeout(resolve, 1000));
  } else {
    console.error("Invalid email address");
  }
}

 

위에 보이는 코드가 더럽다는 느낌을 받으시나요?

 

코드는 한 가지 일만 하도록 단일책임만 갖도록 작성을 해야하지만 위에 코드는 입력 받은 이메일의 유효성 검사와 동시에 서버에 이메일을 전송하는 액션도 수행하고 있습니다. 

 

지금에야 함수가 짧고 간단 해보이지만 코드가 길어진다면 처음 코드를 보는 사람은 이 함수가 어떤 역할을 하는지 직관적으로 파악하기 힘듭니다. 

그리고 이메일을 보내는 과정에 문제가 생기면 유효성을 검사하는 로직에 문제가 생긴건지 외부 액션에 문제가 생긴지 파악하기 어렵습니다. 

마찬가지로 유효성 검사에대한 테스트 코드도 작성하기 어렵겠죠?

 

type Email = string;

// 계산: 이메일 유효성 검사
function isValidEmail(email: Email): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

// 액션: 이메일 보내기
async function sendEmail(email: Email, content: string): Promise<void> {
  console.log(`Sending email to ${email}: ${content}`);
  // 예시를 위한 가상의 비동기 처리
  await new Promise(resolve => setTimeout(resolve, 1000));
}

 

 

위 코드는 순수함수와 액션을 기준으로 분리를 했습니다.

isValidEmail 함수는 순수함수로 이메일이 입력되면 이메일이 적당한지 판단한 결과물만 내려주는 함수입니다. 

함수 이름과 동작하는 로직이 개발자로 하여금 직관적으로 이해할 수 있고 해당 로직의 테스트 코드를 작성해 내부 로직이 의도된대로 동작하는지 파악할 수 있습니다.

 

정리

순수함수와 액션함수를 분리하면 코드의 가독성과 의도가 드러나 빠르게 코드에 대해서 이해할 수 있습니다. 

그리고 테스트를 해보거나 문제가 발생했을때 순수함수와 액션함수가 모두 작성되어 있는 함수의 경우 두가지가 동시에 성공하는지 파악해야 하지만 순수함수와 액션함수로 분리함으로서 효율적으로 테스트를 해 볼 수 있습니다.