0. 프로그래밍 패러다임
- 프로그래밍 패러다임은 프로그래밍을 어떻게 접근하고 문제를 해결하는데 사용되는 기본적인 철학이나 방법론을 의미함
- 간단히 말하자면, 프로그래머들이 코드를 작성하는 방식과 관련된 규칙과 원칙의 집합이다.
- 절차지향 프로그래밍(프로그램을 일련의 절차나 단계로 구분하고, 각 단계에 필요한 기능을 함수로 구현하는 방식 ex: C언어), 객체지향 프로그래밍, 함수형 프로그래밍, 함수형 반응형 프로그래밍과 같은 것들을 프로그래밍 패러다임이라고 한다.
프로그램을 만들 때도 원하는 목표에 따라서, 적절한 방법과 기법을 활용해서 프로그램을 만들어나가게 된다.
함수형 특화된 언어는 아래와 같다.
객체지향 프로그래밍 + 함수형 프로그래밍에서 사용하는 몇가지 특징을 접목한 프로그래밍 언어가 있다.
1. 함수형 프로그래밍
- 함수형 프로그래밍은 수학의 원리와 밀접하게 관련이 있다. 수학에서는 주어진 데이터를 처리해서 결과값을 도출한다. input을 넣으면 어떠한 처리과정을 거쳐서 output 이 나오게 된다.
외부에서는 파이프라인 내부의 상태를 보거나 접근할 수 없다. 파이프라인 안에서도 밖에 접근할 수 없다.
즉, 함수형프로그래밍은 이런 함수들을 적용하고, 묶어서 프로그램을 구성해 나가는 것을 말한다.
🔸함수형 프로그래밍의 필수요소
1) 순수함수를 사용하여 side-effect 를 지양할 것
2) 데이터 불변성을 유지할것
3) 순수함수를 조합하여 선언적 패턴을 가질것
1) 순수함수를 사용하여 side-effect 를 지양할 것
side-effect(부수효과) 란 함수 외부의 데이터를 변경하는 것을 말한다. 즉, 함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우이다. 의도치 않은 결과를 의미하는데, 의도하지 않게 외부변수를 참조하거나 외부변수를 변경하는 모든 종류의 코드를 의미한다.
순수함수 는 side-effect 가 없으며, 인풋에 대한 아웃풋이 항상 같은 함수를 말한다. 즉, 오직 함수의 입력만이 함수의 출력 결과에 영향을 주는 함수이다.
let number = 0;
function pureFunc(num, offset) {
return num + offset;
}
function sideEffect(offset) {
number += offset;
return number;
}
console.log(pureFunc(number, 1)); // 1
console.log(number); // 0 : pureFunc로 인해 값이 달라지지는 않음
console.log(sideEffect(1)); // 1
console.log(sideEffect(1)); // 2: 바로 위 sideEffect()와 input 이 같으나 결과는 다름
console.log(number); // 2 : sideEffect 함수가 외부의 변수를 변경함
위 코드에서 pureFunc 함수는 인자 num 가 같으면 항상 같은 값을 반환하며, 외부의 상태를 변경하지 않는 함수이다.
하지만 sideEffect 함수는 외부 상태에 의존하기 때문에 같은 인자에 대해 다른 값을 반환할 수 있다. 뿐만 아니라 외부의 상태를 함수 내부에서 바꾸게 된다. 이런 side-effect 는 상태변경을 추적하기 어렵게 만들어서 가독성을 해친다. 코드를 자세히 읽어야 상태변경이 일어나는지 확인할 수 있기 때문이다.
하지만 프론트엔드에서 필수적인 작업인 DOM 조작, 서버와의 통신도 side-effect 에 해당한다.
따라서 오로지 순수함수만 사용하는 것은 불가능하다.
최대한 순수함수 위주로 작성하여 의도치 않은 상태변경을 막고 코드를 간결하게 만드는 것이 JavaScript 의 함수형 프로그래밍이다. 논리를 간단하게 만들어야 버그가 스며들지 못한다.
🔸React 에서 대표적인 Side Effect
- 서버에서 API 호출 (fetch API : 네트워크의 상황과 서버상태에 따라 응답코드가 달라지기 때문에 예측불가능 응답 상태코드가 200,404 등 달라질 수 있고, response 에 대한 결과도 계속 달라질 수 있음)
- 함수 외부 값 변경
- 쿠키 및 브라우저 스토리지 이용
- 브라우저 직접 변경(document,window)
- 시간 관련 함수 사용(setTimeout, setInterval)
🤔왜 순수함수로 코드를 짜야하는가? 순수함수가 많아질수록 코드를 더 쉽게 예측할 수 있다
예제 코드 : 매개변수를 직접 변경하지 않는 순수함수
함수의 매개변수로 들어온 값을 직접 변경하는 것을 피하기만 해도, 순수함수를 만들 수 있습니다. 매개변수에 대한 직접 조작을 피하는 이유는 이 매개변수가 또 어디에 쓰일지 모르기 때문입니다.
어떤 함수가 매개변수 값을 직접 변경한다면, 바뀐 값은 쭉 유지되고 다른 함수의 동작에 영향을 미칠 수 있습니다. 아래 코드를 보시죠.
const num_arr = [1, 2, 3, 4, 5];
// 매개변수의 값을 직접 변경하는 불순함수
const addSixImpure = (arr) => {
// 매개변수에 직접 6 추가
arr.push(6);
return arr;
};
// 매개변수를 복사한 값을 변경하는 순수함수
const addSixPure = (arr) => {
// 펼침 연산자로 새로운 배열에 6 추가
newArr = [...arr, 6];
return newArr;
};
// 매개변수 arr에 6이 있는지 확인하는 함수
const hasSix = (arr) => {
if (arr.includes(6)) {
return true;
} else {
return false;
}
};
const new_arr = addSixImpure(num_arr);
console.log(hasSix(num_arr)); // true
addSixPure()과 addSixInpure()는 언뜻 보면 별 차이가 없어 보이지만, addSixInpure()는 매개변수의 값을 직접 변경하는 불순함수이고, addSixPure()는 매개변수 값을 복사해서 변경하는 순수함수입니다.
addSixInpure()는 num_arr을 직접 바꿨기 때문에 함수가 실행되면 num_arr의 값이 [1,2,3,4,5,6]으로 영구히 바뀝니다. 그래서 hasSix()함수의 결과로는 true를 반환하게 되죠.
하지만 개발자의 의도가 변수 new_arr에 addSix 함수를 호출한 새로운 배열을 할당하고 난 후, 값이 [1,2,3,4,5]인 num_arr에 대해서 6이 있는지 판단하고 싶었던 거였다면 코드는 의도대로 실행되지 않았습니다. 의도대로라면 false가 나와야 하는데, num_arr이 이미 변경되었기 때문입니다. 따라서 이런 경우에는 addSix함수가 num_arr을 직접 변경해서는 안됩니다. 대신에 이런 코드가 필요하죠
const new_arr = addSixPure(num_arr);
console.log(hasSix(num_arr)); // false
addSixPure()는 num_arr을 직접 조작하지 않기 때문에 num_arr에 처음 할당되었던 값은 바뀌지 않습니다. 따라서 hasSix(num_arr)의 결과는 false입니다.
이 예시는 순수함수가 많아질수록 코드를 더 쉽게 예측할 수 있음을 알 수 있게 해줍니다. addSixInpure()가 6을 num_arr에 추가해버린 부수효과 때문에 hasSix()의 결과가 부정확해졌습니다. 선언된 변수들을 직접 조작하지 않을수록 함수들은 부수효과 없이 개발자의 의도대로 움직일 가능성이 큽니다.
2) 데이터의 불변성
데이터를 불변객체로 다루는 것을 선호한다. 즉, 데이터가 한 번 생성되면 그 값을 변경하지 않는 것을 의미한다. 이로인해 데이터 변경으로 인한 오류를 줄일 수 있다.
JavaScript 에서 함수에 인수를 전달할 때 원시값(number, string, boolean 등) 은 값에 의한 전달이 되지만 객체는 참조에 의한 전달방식으로 전달된다. 객체를 함수에 전달하면 함수에서 객체의 내부를 바꿀 수 있다는 뜻이다.
const obj = {name : "sujeong", id: "sj_dev_js"};
const addNickname = (o) => {
o.nickname = "sujpark";
}
addNickname(obj);
console.log(obj); // {name: "sujeong", id: "sj_dev_js", nickname: "sujpark"}
위에서 말한 바와 같이 순수함수는 외부의 상태(obj)를 변경해서는 안된다.
위 코드를 순수함수로 작성하면 다음과 같다.
const obj = {name : "sujeong", id: "sj_dev_js"};
const addNicknamePure = (o) => {
return {...o, nickname: "sujpark"}; // 새로운 객체 생성
}
const newObj = addNicknamePure(obj);
console.log(obj); // {name: "sujeong", id: "sj_dev_js"}
console.log(newObj); // {name: "sujeong", id: "sj_dev_js", nickname: "sujpark"}
JavaScript 에서는 배열 또한 객체이기 때문에 같은 방식을 따른다.
JavaScript 는 함수형 프로그래밍을 위한 고차함수들 (map, filter, slice 등) 을 제공하기 때문에 적절히 사용하면 된다.
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const mappedArr = arr.map(el => el + 1);
const filteredArr = arr.filter(el => el % 2 === 0)
const slicedArr = arr.slice(0, 2);
console.log(mappedArr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(filteredArr); // [0, 2, 4, 6, 8]
console.log(slicedArr); // [0, 1]
console.log(arr); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// 원본함수 arr는 변하지 않는다.
3) 순수함수를 조합하여 선언적 패턴을 가질것
- 선언적declarative 프로그래밍은 무엇을 할것인가에 집중한다. 코드가 어떻게 실행되는지에 대한 구체적인 명령을 작성하지 않고, 무엇을 원하는지 명시한다. 어떻게 동작하는지에 대한 구체적인 세부 사항을 숨기고, 추상화된 레벨로 문제를 해결한다.
- 명령형imperative 프로그래밍은 어떻게 할 것인가에 집중한다. 문제 해결방법을 직접 명령하고, 컴퓨터에게 명시적으로 어떤 일을 수행해야하는지 명령한다. 코드가 어떻게 실행되는지 세부 사항을 명시하기에 코드 가독성이 떨어질 수 있다.
const names = ["Han", "Chewbacca", "Luke", "Leia"];
// 명령형 패턴
const shortNames = [];
for (let i = 0; i < names.length; i++) {
if (names[i].length < 5) {
shortNames.push(names[i]);
}
}
// 선언적 패턴
const shortNames = names.filter(name => name.length < 5);
선언적 패턴은 세부적인 구현 방법이 아니라 다루는 문제의 도메인에 초점을 맞춥니다. 이 패턴은 보통 더 간결하고, 더 에러가 적은 코드를 생상합니다. 또한, 이해하기 훨씬 쉽다.
JavaScript는 map, filter, reduce등의 유용한 고차 higher-order functions 함수들을 가지고 있습니다. 선언적 코드를 작성할 때 필수적이니 광범위하게 사용하자
참고자료
https://www.youtube.com/watch?v=4ezXhCuT2mw
https://80000coding.oopy.io/d486a93b-7619-4006-8431-241a6c10cc45
'프론트엔드 개발 > JavaScript' 카테고리의 다른 글
자바스크립트) 객체지향 프로그래밍 (0) | 2023.06.19 |
---|---|
자바스크립트) 프로토타입 (0) | 2023.06.19 |
자바스크립트) 일급객체 (0) | 2023.06.12 |
자바스크립트) 생성자 함수 (0) | 2023.06.08 |
자바스크립트) this (0) | 2023.06.08 |