순수 함수와 보조 함수의 조합을 통해 외부 상태를 변경하는 부수 효과를 최소화해서 불변성(immutability)을 지향하는 프로그래밍 패러다임
저는 처음 함수형 프로그래밍을 정의하는 위의 문장이 잘 이해가지 않아
어떤 것이 함수형 프로그래밍 답게 코드를 작성하는 것이고
어떤 방식으로 함수형 프로그래밍을 구현할 수 있는지 알아보고자 이 글을 쓰게 되었다.
본격적으로 함수형 프로그래밍에 대해 알기 전에 알아야하는 두가지 개념
명령형 프로그래밍은 문제를 어떻게 해결할 것인지 컴퓨터에게 명시적으로 명령을 내리는 방법을 의미한다.
선언형 프로그래밍은 무엇을 해결할 것인지에 집중하고 어떻게 문제를 해결하는지에 대해서는 컴퓨터에게 위임하는 방법이다.
위와 같이 두가지 프로그래밍 패러다임에 대해 알아보았는데 함수형 프로그래밍은 선언형 프로그래밍의 대표격 패러다임이라고 할 수 있다.
선언형 프로그래밍과 함수형 프로그래밍을 그렇다면 정확히 어떤 관계일까?
선언형 프로그래밍은 명령형 프로그래밍과 다르게 해결할 문제에 대한 방법을 컴퓨터에게 명시적으로 지시하는 것이 아닌 위임한다고 했다.
이렇게 위임을 통해 개발자는 무엇을 해결할지에 집중할 수 있는 것인데
그렇다면 어떤 방법으로 컴퓨터에게 해결할 문제에 대한 방법을 위임할 수 있을까?
바로 추상화를 통해 컴퓨터에게 내부 로직을 넘겨주는 것이다.
추상화에 대해 알아보자
- 특정 존재의 다양한 속성 중 필요한 속성만을 간추려내어 표현한 것
- 구체적인 정보를 숨기고 꼭 필요한 핵심만을 뽑아내서 표현하는 방식
- 복잡한 것들을 목적에 맞게 단순화하는 것!
추상화라는 단어는 개념적인 의미로 받아들이면 된다.
위의 세가지 개념으로 이해해도 되지만 간단히 말하자면 복잡한 무언가를 단순해 보이도록 만들어주는 행위 라고 이해하면 된다.
이쯤에서 다시 선언형 프로그래밍에 대해 다시 정리하면
추상화를 통해 컴퓨터에게 문제를 어떻게 해결할 것인지를 위임하고 개발자는 무엇을 해결할 것인지에 집중할 수 있게하는 패러다임
그렇다면 다시금 선언형 프로그래밍과 함수형 프로그래밍은 어떤관계일까?
함수로 추상화를 구현해서 컴퓨터에게 문제를 어떻게 해결할 것인지를 위임하고 개발자는 무엇을 해결할 것인지에 집중할 수 있게하는 패러다임
즉, 추상화라는 과정을 함수로써 구현하는 것이 함수형 프로그래밍이다.
배열을 순회하며 빈 문자열을 걸러내고 각 원소의 첫 글자를 대문자로 변경해라
말로만 설명하면 이해가 어려우니 간단한 예제를 통해 이해를 도우려 한다.
만약 위의 문제를 명령형 프로그래밍으로 구현하면 어떤식으로 구현할 수 있을까?
아마 대부분의 개발자는 이런 문제를 만났을 때 for
반복문을 사용하여 순차적으로 배열의 원소를 탐색하고 조건을 처리하는 방식으로 코드를 작성할 것이다.
const people = ['kim', 'son', 'park', ''];
const register = [];
for (let i = 0; i < people.length; i++) {
if (people[i].length === 0) continue;
register.push(
people[i].replace(people[i][0], people[i][0].toUpperCase())
);
}
이렇게 for
문을 사용하여 특정 행위를 반복하는 코드를 작성하는 일은 개발자라면 해석하는데 큰 어려움이 없는 코드지만 너무 익숙한 나머지 이렇게 짧은 코드에도 많은 명령이 들어 있다는 사실을 간과하고는 한다.
정리해보자면
- 변수
i
를0
으로 초기화한다.i
가people
배열의 길이보다 작다면 구문을 반복 실행한다.for
문 내부의 코드의 실행이 종료될 때마다i
에1
씩 더한다.- 만약 원소의 길이가
0
이라면continue
즉, 다음 반복문을 실행한다.- 원소의 0번 인덱스의 글자를 대문자로 변경한 문자와 교체한다.
- 이렇게 합쳐진 문자열을 결과물 배열이 될
register
배열에 삽입한다,
...만약 문제를 보지 않고 해당 코드만 보고 문제가 무엇이었는지 유추하려면 어려울 것이다.
이렇게 짧은 코드에도 변수선언과 조건문, 부수효과가 발생하며 해당 반복문이 일어나기 전과 후에 register
배열이 변경가능하다는 위험성이 있고 몇 번 반복해야되는지 생각해야되는 문제점이 있다.
물론 앞에서도 언급했듯이 뛰어난 개발자라면 별거 아니라고 할 수도 있지만 유지보수의 관점에서 보면 새로운 개발자가 이전의 코드를 분석하려면 시간이 상당히 소요된다는 것을 의미한다.
그렇다면 명령형 프로그래밍으로 작성된 위의 코드를 추상화를 통해 선언형 프로그래밍으로 작성해보기전 추상화 과정을 먼저 가져보도록 하겠다.
배열을 순회한다
- 변수
i
를0
으로 초기화한다.i
가people
배열의 길이보다 작다면 구문을 반복 실행한다.for
문 내부의 코드의 실행이 종료될 때마다i
에1
씩 더한다.
빈 문자열을 필터링한다
- 만약 원소의 길이가
0
이라면continue
즉, 다음 반복문을 실행한다.
첫 글자를 대문자로 변경한다
- 원소의 0번 인덱스의 글자를 대문자로 변경한 문자와 교체한다.
결과물 배열을 만든다
- 이렇게 합쳐진 문자열을 결과물 배열이 될
register
배열에 삽입한다,
배열을 순회하며 빈 문자열을 필터링하고 첫 글자를 대문자로 변경한 배열을 반환한다.
한 문장이 되었다.
그렇다면 이를 함수형 프로그래밍으로 작성해보겠다.
const register = people.filter(name => name.length).map(name => name.replace(name[0], name[0].toUpperCase()));
혹시 처음 보고 어떤 인상이 들었는가?
코드에는 첫인상이라는 말이 있다.
감히 말하지만 아마 JS를 어느정도 공부한 개발자라면 편안함을 느꼈을 것이다.
위의 코드는 한줄로써 문장처럼 읽는 것이 가능하다.
people이라는 배열을 filter를 통해 길이가 있는 요소만 담은 배열을 반환하고 map을 통해 첫글자를 대문자로 바꾼 요소들을 담은 배열을 반환한다.
즉, 순수함수로만 이루어져 있다.
부수효과가 없으면 원본 배열인 people
을 안전하게 보호할 수 있고 어떤 외부참조도 없기 때문에 안전성이 보장된다.
즉, register
는 전달받은 배열을 무조건 빈 문자열이 아닌 문자의 첫 글자를 대문자로 바꾼 배열을 반환한다라고 선언할 수 있다.
모든 것은 일장일단이다.
함수형 프로그래밍이 정답은 아니다라는 것이다.
위의 명령형 프로그래밍으로 작성된 코드의 시간복잡도는 O(1n)이다.
(n앞의 숫자는 의미 없지만 비교를 위해 명시했다.)
반면 함수형 프로그래밍으로 작성된 코드의 시간복잡도는 O(2n)이다.
즉, 성능 상 명령형에 비해 조금 뒤쳐진다라는 것을 의미한다.
하지만 명령형으로 작성된 방대한 코드와 함수형으로 작성된 방대한 코드를 만났을 때 유지보수의 관점에서 보자면 성능이 조금 떨어지더라도 충분히 감수할만한 부분이라고 생각한다.
성능차이가 커진다면 얘기는 달라지겠지만 미미한 차이라면 유지보수가 용이한 함수형 프로그래밍이 더 좋지 않을까? 라는 것이 개인적인 생각이다.
이런 나의 개인적인 의견이 명령형 프로그래밍이 좋지 않다는 것은 절대 아니다
하지만 프론트엔드 개발자를 희망하고 있는 나로써는 함수형 프로그래밍은 충분히 매력적인 패러다임으로 받아들여졌다, 라는 것이 이 글의 결론이다.