일급 함수, 고차 함수, Copy-on-Write
함수형 프로그래밍에서 함수는 단순한 코드 블록이 아니라 값으로 다뤄집니다. 이 글에서는 일급 함수, 고차 함수의 개념과 Copy-on-Write 패턴을 통한 실전적인 불변성 유지 방법을 알아봅니다.
일급 함수 (First-Class Function)
섹션 제목: “일급 함수 (First-Class Function)”일급 함수란 함수를 다른 값처럼 다룰 수 있다는 의미입니다. 변수에 저장하거나, 배열에 넣거나, 다른 함수의 인자로 전달할 수 있습니다.
// 함수를 배열에 저장const funcs = [() => 1, () => 2];console.log(funcs[0]()); // 1// 함수를 인자로 전달function mapFunc(arr, callback) { const newArr = []; for (const elem of arr) { newArr.push(callback(elem)); } return newArr;}
const result = mapFunc([1, 2, 3], (num) => num + 1);// [2, 3, 4]고차 함수 (Higher-Order Function)
섹션 제목: “고차 함수 (Higher-Order Function)”고차 함수는 함수를 인자로 전달받거나, 함수를 리턴하는 함수입니다.
function doTwice(func) { func(); func();}
doTwice(() => console.log("hi"));// "hi"// "hi"React에서의 고차 함수: HOC
섹션 제목: “React에서의 고차 함수: HOC”React에서 고차 컴포넌트(Higher-Order Component)는 고차 함수의 대표적인 활용 사례입니다.
export default function withAuthentication(WrappedComponent) { const AuthenticatedComponent = (props) => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [error, setError] = useState(false);
useEffect(() => { (async function () { try { await fetch("/login"); setIsAuthenticated(true); } catch { setError(true); } })(); }, []);
return ( <> {isAuthenticated ? ( <WrappedComponent {...props} /> ) : ( <div>Loading...</div> )} </> ); };
return AuthenticatedComponent;}컴포넌트 위에 로그인을 확인하는 컴포넌트를 레이어처럼 한 겹 더 올리는 느낌입니다. 여러 컴포넌트에서 로그인 상태를 조회하는 로직이 필요할 때 유용하지만, 요즘은 Custom Hook으로 대체하는 추세입니다.
Arity (인수의 수)
섹션 제목: “Arity (인수의 수)”Arity란 함수가 취하는 인수의 수를 의미합니다. 함수 합성에서 인수가 다른 함수들을 조합할 때 문제가 됩니다.
const user = getUser(users, "henry");const user1 = updateScore(cloneObj(user), 30);const user2 = updateTries(cloneObj(user1));const newArr = storeUser(users, user2);
// 각 함수마다 인수가 다르므로 pipe 함수를 바로 사용할 수 없음const updateUser = pipe(/* ??? */);bind()를 활용한 부분 적용
섹션 제목: “bind()를 활용한 부분 적용”bind()는 apply(), call()과 달리 함수를 바로 실행하지 않고 새로운 함수를 생성합니다.
const partGetUser = getUser.bind(null, users);const user = partGetUser("henry");
const partUpdateScore30 = updateScore.bind(null, 30);
const updateUser = pipe( partGetUser, cloneObj, partUpdateScore30, updateTries)("henry");**부분 적용(Partial Application)**이란 함수의 일부 인자만 적용되고, 아직 모든 인자가 적용되지 않은 상태의 함수입니다. 일부 인자가 함수의 클로저 스코프에 고정된 상태로 유지됩니다.
React에서 bind 사용 시 주의점
섹션 제목: “React에서 bind 사용 시 주의점”onClick={handler.bind(null, e.target)}으로 사용할 경우 e.target은 이벤트 발생 시점이 아니라 컴포넌트 렌더링 시점에 평가됩니다. 이벤트 핸들러에서는 onClick={(e) => handler(e)}를 사용해야 합니다.
액션과 계산의 분리
섹션 제목: “액션과 계산의 분리”함수형 프로그래밍에서 가장 중요한 실천은 액션에서 계산을 분리하는 것입니다.
분리 원칙
섹션 제목: “분리 원칙”- 액션, 계산, 데이터로 나눠서 고민
- 액션에서 최대한 계산을 분리할 것
- 계산에서는 데이터를 분리
- 계산은 더 작은 계산과 데이터로 나누고 연결할 수 있다
분리 실습: 장바구니 예시
섹션 제목: “분리 실습: 장바구니 예시”// Before - 전역 상태를 직접 변경하는 함수들let currentUser = 0;const users = [ { name: "james", score: 30, tries: 1 }, { name: "mary", score: 110, tries: 4 }, { name: "henry", score: 80, tries: 3 },];
const updateScore = (newAmt) => (users[currentUser].score += newAmt);const updateTries = () => users[currentUser].tries++;// After - 순수 함수로 분리const getScore = (arr, name) => { const targetObj = arr.find( (user) => user.name.toLowerCase() === name.toLowerCase() ); return [name, targetObj.score];};
const updateScore = (arr, amt) => { const newAmt = arr[1] + amt; return [arr[0], newAmt];};
const updateTries = (arr) => { const newTries = arr[1] + 1; return [arr[0], newTries];};
// 부수효과가 있는 함수는 별도로 분리const recordData = (arr, prop) => { users.forEach((val, index, array) => { if (val.name.toLowerCase() === arr[0].toLowerCase()) { array[index][prop] = arr[1]; } });};핵심 배움:
- 기본 동작을 하는 함수부터 만든다
- 해당 함수의 반환값을 이용해 점점 고차 함수로 기능을 확장한다
- 부수효과를 다루는 함수를 따로 분리해서 점점 더 큰 함수로 확장한다
Copy-on-Write
섹션 제목: “Copy-on-Write”Copy-on-Write는 데이터를 변경할 때 복사본을 만들어 변경하고 반환하는 패턴입니다.
배열의 Copy-on-Write
섹션 제목: “배열의 Copy-on-Write”// Before - 원본 배열을 직접 변경let mailing_list = [];
function add_contact(email) { mailing_list.push(email);}
// After - Copy-on-Write 적용const add_elem = (list, email) => { const listCopy = list.slice(); listCopy.push(email); return listCopy;};
const submit_form_handler = (event) => { const form = event.target; const email = form.elements["email"].value; mailing_list = add_elem(mailing_list, email);};읽기와 쓰기 분리
섹션 제목: “읽기와 쓰기 분리”쓰기를 하면서 읽기도 하는 동작은 분리합니다:
// 읽기function first_element(array) { return array[0];}
// 쓰기 - Copy-on-Write 적용function drop_first(array) { const array_copy = array.slice(); array_copy.shift(); return array_copy;}
// 두 값을 모두 반환function shift(array) { return { first: first_element(array), array: drop_first(array), };}pop() 분리 예시
섹션 제목: “pop() 분리 예시”function last_element(array) { return array[array.length - 1];}
function drop_last(array) { const newArr = array.slice(); newArr.pop(); return newArr;}
function pop_fake(array) { return { last: last_element(array), array: drop_last(array), };}push의 Copy-on-Write
섹션 제목: “push의 Copy-on-Write”function push(array, elem) { return [...array, elem];}
function add_contact(mailing_list, email) { return push(mailing_list, email);}객체의 Copy-on-Write
섹션 제목: “객체의 Copy-on-Write”Object.assign({}, obj) 또는 스프레드 연산자를 사용합니다.
function objectSet(object, key, value) { const newObject = Object.assign({}, object); newObject[key] = value; return newObject;}
function setPrice(item, new_price) { return objectSet(item, "price", new_price);}
function setQuantity(item, new_quantity) { return objectSet(item, "quantity", new_quantity);}객체 삭제의 Copy-on-Write
섹션 제목: “객체 삭제의 Copy-on-Write”function objectDelete(object, key) { const newObj = Object.assign({}, object); delete newObj[key]; return newObj;}중첩 객체의 Copy-on-Write
섹션 제목: “중첩 객체의 Copy-on-Write”// Before - 원본을 직접 변경function setQuantityByName(cart, name, quantity) { for (var i = 0; i < cart.length; i++) { if (cart[i].name === name) { cart[i].quantity = quantity; } }}
// After - Copy-on-Write 적용function setQuantityByName(cart, name, quantity) { const cart_copy = [...cart]; for (let i = 0; i < cart_copy.length; i++) { if (cart_copy[i].name === name) { cart_copy[i] = { ...cart_copy[i], quantity }; break; } } return cart_copy;}배열뿐만 아니라 내부 객체까지 새로운 객체로 만들어야 합니다.
- 일급 함수는 값처럼 다룰 수 있는 함수로, 함수형 프로그래밍의 기반입니다
- 고차 함수는 함수를 인자로 받거나 반환하여 재사용성을 높입니다
- Copy-on-Write는 데이터를 변경할 때 항상 복사본을 만들어 불변성을 보장합니다
- 액션에서 계산을 분리하고, 부수효과가 있는 코드를 명확히 격리하는 것이 핵심입니다
다음 글에서는 Currying과 함수 합성을 통해 함수들을 조합하는 방법을 알아보겠습니다.