JD의 블로그

Learning React - 3장 본문

Web/프론트엔드

Learning React - 3장

GDong 2021. 10. 6. 01:05

자바스크립트를 활용한 함수형 프로그래밍

자바스크립트 프로젝트에서 함수형 기법의 사용이 점점 늘어나는 중이다.

 

이번 장에서는 함수형 프로그래밍의 핵심 개념을 몇가지 살펴보고, 자바스크립트에서 함수형 기법을 구현하는 방법을 알아본다.

 

자바스크립트에서는 변수에 함수를 대입할 수 있고, 함수를 다른 함수에 인자로 넘길 수 있으며, 함수에서 함수를 만들어서 반환할 수 있다.

그래서 함수를 일바넉인 데이터와 마찬가지로 취급하며 일반적인 값과 동일하게 저장하거나 읽어오거나, 애플리케이션에서 흘려보낼 수 있다.

 

선언적 프로그래밍

선언적 프로그래밍은 필요한 것들 달성하는 과정을 하나하나 기술하는 것보다 필요한 것이 어떤 것인지를 기술하는 것에 더 방점을 두고 애플리케이션의 구조를 세워나가는 프로그래밍 스타일이다.

 

선언전 프로그래밍을 이해하기 위해 명령형 프로그래밍을 알아야한다. 명령형 프로그래밍은 코드로 원하는 결과를 달성해 나가는 과정에만 관심을 두는 프로그래밍 스타일이다. 

 

명령형 프로그램의 예시 (문자열을 URL에서 사용할 수 있게 만들기 위해 모든 공백을 하이픈으로 바꾸는 것)

const string = " Restaurants in Hanalei";
const urlFriendly = "";

for (var i=0; i<string.length; i++){
	if(string[i] === " ") {
    	urlFriendly += "-";
    } else {
    	urlFriendly += string[i];
    }
}

console.log(urlFriendly); //"Restaurants-in-Hanalei"

이런 구조의 프로그램은 우리가 원하는 것을 달성하는 방법에만 신경쓴다. 명령형 프로그래밍에서는 코드 안에서 어떤 일이 벌어지는지 코드를 읽는 사람이 이해할 수 있게 돕기 위해 주석을 많이 달 필요가 있다.

 

이제 같은 문제를 선언적으로 푸는 것을 보자

const string = "Restaurants in Hanalei";
const urlFriendly = string.replace(/ /g, "-");
console.log(urlFriendly);

여기서는 string.replace와 정규식을 사용해서 모든 공백을 하이픈으로 바꾼다. 선언적 프로그래밍의 코드 구문은 어떤 일이 발생해야 하는지에 대해 기술하고, 실제로 그 작업을 처리하는 방법은 추상화를 통해 아랫단에 감춰진다. 

 

리액트 컴포넌트를 사용해 DOM을 선언적으로 구성하는 방법을 보면

const {render} = ReactDOM;

const Welcome = () => {
	<div id="welcome">
    	<h1>Hello World</h1>
    </div>
);

render(<Welcome />,
	document.getElementById('target'));

리액트는 선언적이다. 여기서 Welcome 컴포넌트는 렌더링 할 DOM에 대해 기술한다. render 함수는 컴포넌트에 있는 지시에 따라 DOM을 만들며 이 과정에서 실제 DOM이 어떻게 만들어져야 하는지에 대한 내용은 추상화를 통해 사라진다.

 

함수형 프로그래밍의 개념

1. 불변성

함수형 프로그래밍에서는 데이터가 변할 수 없다. 불변성 데이터는 결코 바뀌지 않는다.

원본 데이터 구조를 변경하고 싶을 때 원본을 안전하게 유지하기 위해 데이터 구조의 복사본을 만들되 그 중 일부를 변경한다. 

var rateColor = function(color, rating) {
	return Object.assign({}, color, {rating:rating});
};

console.log(rateColor(color_lawn, 5).rating); //5
console.log(color_lawn.rating);

여기서 Object.assign은 복사기와 같은데 Object.assign은 빈 객체를 받고, color객체를 그 빈 객체에 복사한 다음에, 복사본에 있는 rating 프로퍼티의 값을 rating 파라미터의 값으로 변경한다. 이를 통해 원본은 그대로 남겨둔 채, rating만 변경된 복사본을 손에 쥐게 된다. 

 

이를 보다 간단하게 만들기 위해서 스프레드 연산자를 사용할 수도 있다.

const rateColor = (color, rating) => ({
	...color,
    rating
});

반환할 객체에 괄호로 감싼 것을 주의하자. 화살표 함수의 본문에서 바로 중괄호를 사용해 객체를 반환할 수 없기 때문에 괄호가 꼭 필요하다.

 

함수의 원본 배열에 새로운 원소를 추가하는 경우

let list = [
	{ title: "과격한 빨강"},
    { title: "잔디"},
    { title: "파티 핑크"}
];

const addColor = (title, array) => array.concat({title});

console.log(addColor("화려한 녹색", list).length); 
console.log(list.length);

원래의 list 배열을 변화시키지 않고 유지하기 위해서는 Array.concat을 사용해야한다. Array.concat은 그 객체를 원래의 배열을 복사한 새로운 배열 뒤에 추가한다.

 

이를 스프레드 연산자로 축약하면 다음과 같다.

const addColor = (title, list) => [...list, {title}];

이 함수는 원본 list의 원소들을 새로운 배열에 복사하고, title 파라미터로 받은 값을 title 프로퍼티로 하는 객체를 새 배열 뒤에 추가한다. 

 

 

2. 순수 함수

순수 함수는 파라미터에 의해서만 반환값이 결정되는 함수를 말한다. 순수 함수는 최소한 하나 이상의 인수를 받고, 인자가 같으면 항상 같은 값이나 함수를 반환한다. 순수 함수는 인수를 변경 불가능한 데이터로 취급한다.

 

순수 함수는 자연적으로 테스트하기 쉬운데 함수에 전달되는 인수만 제어하면 되며, 인자에 따른 결괏값을 예상할 수 있다. 

 

const frederick = {
	name: "Frederick Douglass",
    canRead: false,
    canWrite: false
};

const selfEducate = (person) => {
	...person,
    canRead = true,
    canWrite = true
});

console.log( selfEducate(frederick) );
console.log( frederick );

// {name: "Frederick Douglass", canRead: true, canWrite: true}
// {name: "Frederick Dougalss", canRead: false, canWrite: false}

새 값을 계산할 때 전달받은 인자를 변경하지 않고 새로 만든 객체를 반환한다. 

 

리액트에서는 UI를 순수 함수로 표현한다. 다음 예제는 Header는 머릿글을 만들어내는 순수 함수이다. 이 함수는 DOM을 변경하는 부수 효과를 발생시키지 않고 엘리먼트를 반환한다. 이 함수는 엘리먼트를 만드는 일만 책임지며, DOM을 변경하는 책임은 애플리케이션의 다른 부분이 담당해야 한다.

const Header = (props) => <h1>{props.title}</h1>;

순수 함수를 사용하면 애플리케이션의 상태에 영향을 미치지 않기 때문에 코딩이 편해진다. 함수를 사용할 때 다음 세 가지 규칙을 따르면 순수 함수를 만들 수 있다.

  • 순수 함수는 파라미터를 최소 하나 이상 받아야 한다.
  • 순수 함수는 값이나 다른 함수를 반환해야 한다.
  • 순수 함수는 인자나 함수 밖에 있는 다른 변수를 변경하거나, 입출력을 수행해서는 안된다.

3. 데이터 변환

함수형 프로그래밍은 한 데이터를 다른 데이터로 변환하는 것이 전부다. 

함수형 프로그래밍은 함수를 사용해 원본을 변경한 복사본을 만들어내고 순수 함수를 사용해 데이터를 변경하면, 코드가 덜 명령형이 되고 그에 따라 복잡도도 감소한다. 

 

함수형 자바스크립트를 유창하게 사용하기 위해 통달해야 하는 핵심 함수가 Array.map과 Array.reduce이다.

 

Array.join은 배열의 모든 원소를 인자로 받은 구분자로 연결한 문자열을 반환한다. 원래의 배열은 그대로 남는다. 프로그래머가 join이 제공하는 추상화를 사용하면 문자열을 실제로 어떻게 만드는지에 대해서 신경쓰지 않아도 된다.

 

Array.filter는 원본 배열로부터 새로운 배열을 만들어내는 자바스크립트 배열 내장함수이다. 이 함수는 불린 값 true나 false를 반환하는 함수를 유일한 인자로 받는다. Array.filter는 배열에 있는 모든 원소를 하나씩 사용해 술어를 호출한다. filter는 술어에 배열의 원소를 인자로 전달하며 술어가 반환하는 값이 true이면 해당 원소를 새 배열에 넣는다. 

배열에서 원소를 제거할 필요가 있다면 Array.pop이나 Array.slice보다 Array.filter를 사용하는게 좋다. 

 

Array.map은 술어가 아니라 변환 함수를 인자로 받는다. 

const highSchools = schools.map(school => `{school} High School`);

console.log(highSchools.join("\n"));

// Yorktown High School
// Washington & Lee High School
// Wakefield High School

console.log(schools.join("\n"));

// Yorktown
// Washigton & Lee
// Wakefield

이 경우 map 함수는 각 학교 이름 뒤에 'High School'을 추가한다. 이때 원본 schools 배열은 아무 변화가 없다.

 

객체를 배열로 변환하고 싶을 때는 Array.map과 Object.keys를 함께 사용하면 된다. Object.keys는 어떤 객체의 키로 이뤄진 배열을 반환하는 메서드다.

 

const schools = {
	"Yorktown": 10,
    "Washington & Lee" : 2,
    "Wakefield": 5
};

const schoolArray = Object.keys(schools).map(key => ({
	name: key,
    wins: schools[key]
})
);

 

reduce와 reduceRight 함수를 사용하면 객체를 수, 문자열, 불린 값, 객체, 심지어 함수와 같은 값으로 변환할 수 있다.

수로 이뤄진 배열에서 최댓값을 찾는 경우를 보자.

const ages = [21,18,42,40,64,63,34];

const maxAge = ages.reduce((max,age) => {
	console.log(`${age} > ${max} = ${age > max}`);
    if(age > max) {
    	return age;
    } else {
    	return max;
    }
}, 0);

reduce 함수는 변환 함수와 초깃값을 인자로 받는다. 여기서 초깃값은 0이고 처음에 그 값으로 최대값 max를 설정한다. 처음 변환 함수가 호출 될 때는 age가 배열의 첫 번째 원소인 21이고 max는 초깃값이 0이다. 변환 함수는 0과 21중 더 큰 값인 21을 반환한다. 이 반환값인 21이 다음 이터레이션 시 max 값이 된다.

 

앞의 함수에서 if/else를 짧게 변경하면 다음 코드를 사용해 배열의 최댓값을 계산할 수 있다.

const max = ages.reduce((max, value) => (value > max ? value : max), 0);

Array.reduceRight는 Array.recude와 같은 방식으로 동작하지만 배열의 첫 번째 원소부터가 아니라 맨 마지막 원소부터 축약을 시작한다는 점이 다르다.

 

4. 고차 함수

고차함수는 다른 함수를 인자로 받을 수 있거나 함수를 반환할 수 있고 때로는 그 2가지를 모두 수행한다.

고차 함수의 첫 번째 분류는 다른 함수를 인자로 받는 함수다.

Array.map, Array.filter, Array.reduce는 모두 다른 함수를 인자로 받는 고차 함수다.

 

 

invokeIf 함수는 조건을 검사해서 조건이 참인 경우 fnTrue 함수를 조건이 거짓인 경우 fnFalse 함수를 호출한다.

const invokeIf = (condition, fnTrue, fnFalse) => 
	(condition) ? fnTrue() : fnFlase();

const showWelcome = () => 
	console.log("Welcome!!");

const showUnauthorized = () => 
	console.log("Unauthorized!!!");
    
invokeIf(true, showWelcome, showUnauthorized); // "Welcome"
invokeIf(flase, showWelcome, showUnauthorized); // "Unauthrozied"

다른 함수를 반환하는 고차 함수는 자바스크립트에서 비동기적인 실행 맥락을 처리할 때 유용하다. 함수를 반환하는 고차 함수를 쓰면 필요할 때 재활용할 수 있는 함수를 만들 수 있다.

 

커링(Curring)은 고차 함수 사용법과 관련한 함수형 프로그래밍 기법이다. 커링은 어떤 연산을 수행할 때 필요한 값 중 일부를 저장하고 나중에 나머지 값을 전달받는 기법이다. 이를 위해 다른 함수를 반환하는 함수를 사용하며, 이를 커링된 함수라 부른다.

 

userLogs는 일부 정보(사용자 이름)를 받아 함수를 반환한다. 나머지 정보(메시지)가 사용 가능해지면 userLogs가 반환한 함수를 활용할 수 있다. 이 예제에서는 연관된 사용자 이름 정보 뒤에 로그 메시지를 붙인다. 

 

const userLogs = userName => message => 
	console.log(`${userName} -> ${message}`);

const log = userLogs("grapdpa23");

log("attempted to load 20 fake members")


getFakeMembers(20).then(
	members => log(`successfully loaded ${members.length} members`),
    error => log("encountered an error loading members")
);

 

userLogs는 고차 함수라서 userLogs를 호출해 만들어지는 log함수를 호출할 때마다 메시지 맨 앞에 grandpa23이 덧붙여진다.

 

5. 재귀

재귀는 자기 자신을 호출하는 함수를 만드는 기법이다.

 

10부터 0까지 거꾸로 세는 경우

const countdown = (value, fn) => {
	fn(value);
    return (value > 0) ? countdown(value-1, fn) : value;
};

countdown(10, value => console.log(value));

재귀는 비동기 프로세스에서도 잘 작동하는 또 다른 함수형 기법이다. 

 

데이터 구조를 검색할 때도 재귀가 유용하다. 어떤 폴더의 모든 하위 폴더를 뒤져 가면서 파일 이름을 모두 추려내고 싶다면 재귀를 사용할 수 있다. HTML DOM에서 자식이 없는 엘리먼트를 찾고 싶을 때도 재귀를 쓸 수 있다. 

 

const deepPick = (fields, object={}) => {
	const [first, ...remaining] = fields.split(".");
    return (remaining.length) ? 
    	? deepPick(remaining.join("."), object[first])
        : object[first];
};

deepPick 함수는 값을 반환하거나 자기 자신을 재귀 호출한다. 재귀 호출은 값이 발견될 떄까지 반복된다. 먼저 이 함수는 점으로 구분한 문자열을 배열로 변환하고 그 배열의 첫 번째 값과 나머지를 배열 구조 분해를 사용해 분리한다. 배열에 남은 값이 있다면 deepPick은 약간 다른 데이터를 가지고 자기 자신을 다시 호출해서 객체 계층 구조의 한 단계 아래를 처리한다.

 

6. 합성

함수형 프로그래밍은 로직을 구체적인 작업을 담당하는 여러 작은 순수 함수로 나눈다. 자세히 말하자면 각 함수를 서로 연쇄적으로 또는 병렬로 호출하거나 여러 작은 함수를 조합해서 더 큰 함수로 만드는 과정을 반복해서 전체 애플리케이션을 구축해야한다. 

 

합성의 경우 여러 구현과 패턴이 있는데 가장 낯익은 것은 함수를 연쇄 호출하는 체이닝이다.

이전 함수의 반환 값에 다음 함수를 적용할 수 있다.

 

const template ="hh:mm:ss tt";
const clockTime = template.replace("hh", "03")
	.replace("mm","33")
    .replace("ss","33")
    .replace("tt","PM");

 

 

'Web > 프론트엔드' 카테고리의 다른 글

Learning React - 6장  (0) 2021.10.11
Learning React - 5장  (0) 2021.10.08
Learning React - 4장  (0) 2021.10.06
Learning React - 2장  (0) 2021.10.04