📚 목차
[React] 함수형 프로그래밍, 프론트엔드 개발에 진짜 도움 될까?
프론트엔드 개발을 하다 보면 “함수형 프로그래밍”이라는 말을 자주 듣게 된다.
React는 함수 컴포넌트를 중심으로 발전했고, JavaScript에서는 map, filter, reduce 같은 함수형 스타일의 메서드도 자주 사용한다. 그래서 자연스럽게 “프론트엔드 개발자는 함수형 프로그래밍을 잘 알아야 하는 걸까?”라는 질문이 생긴다.
토스 모닥불 EP.2에서도 이 주제를 다룬다. 핵심은 단순히 **"함수형 프로그래밍이 좋다"**가 아니다. 오히려 중요한 메시지는 함수형 프로그래밍과 객체지향 프로그래밍을 대립 관계로 볼 필요가 없다는 것이다. 두 패러다임은 서로 다른 관점에서 문제를 바라보는 도구이며, 프론트엔드 개발에서는 둘 다 충분히 유용하게 활용될 수 있다.
함수형 프로그래밍이란 무엇인가?
함수형 프로그래밍, 즉 FP(Functional Programming)은 쉽게 말하면 작은 함수들을 조합해서 프로그램을 구성하는 방식이다.
객체지향 프로그래밍이 "객체"를 중심으로 상태와 행위를 묶어 표현한다면,
함수형 프로그래밍은 입력값을 받아 출력값을 만드는 함수를 중심으로 사고한다.
예를 들어 게시글 목록에서 공개된 글만 골라 카드 형태로 바꾸고 최신순으로 정렬한다고 해보자.
const postCards = posts.filter(isPublicPost).map(toPostCard).sort(byLatest);이 코드는 데이터가 어떤 흐름으로 변환되는지 비교적 명확하게 드러난다.
-> 공개된 글만 필터링
-> 카드 UI에 필요한 형태로 변환
-> 최신순 정렬
이처럼 함수형 프로그래밍은 복잡한 로직을 작은 함수 단위로 나누고, 그 함수들을 연결해 더 큰 동작을 만든다.
Toss의 모닥불 영상에서도 함수형 프로그래밍의 첫인상을 "작고 다양한 부품들을 조립하는 느낌"으로 설명한다.
순수 함수와 사이드 이펙트
함수형 프로그래밍에서 중요한 개념 중 하나는 순수 함수다.
순수 함수는 다음 두 가지 조건을 만족하는 함수다.
-
- 같은 입력이 들어오면 항상 같은 출력을 반환한다.
-
- 함수 밖의 상태를 변경하지 않는다.
예를 들어 다음 함수는 순수 함수다.
function getDiscountedPrice(price: number, rate: number) {
return price * (1 - rate);
}price와 rate가 같다면 언제 호출해도 같은 결과를 반환한다.
또한 외부 상태를 변경하지 않는다.
반대로 다음 함수는 사이드 이펙트를 가진다.
function saveUser(user: User) {
localStorage.setItem('user', JSON.stringify(user));
}이 함수는 localStorage라는 외부 세계를 변경한다.
이런 동작을 사이드 이펙트라고 한다.
프론트엔드 개발에는 사이드 이펙트가 매우 많다.
- API 요청
- localStorage 접근
- DOM 조작
- analytics 이벤트 전송
- Sentry 로깅
- setTimeout / setInterval
- 브라우저 히스토리 변경
- 전역 상태 변경
함수형 프로그래밍이 실무에서 도움 되는 지점은 바로 여기에 있다.
모든 코드를 순수 함수로 만들 수는 없지만, 순수 로직과 사이드 이펙트가 있는 로직을 분리하는 것만으로도 코드의 예측 가능성이 높아진다.
예를 들어 이벤트 payload를 만드는 로직과 실제 analytics를 전송하는 로직을 분리할 수 있다.
function createPageViewPayload(user: User, page: string) {
return {
userId: user.id,
page,
};
}
function sendPageViewEvent(payload: PageViewPayload) {
analytics.track('page_view', payload);
}createPageViewPayload는 순수 함수에 가깝기 때문에 테스트하기 쉽다.
반면 sendPageViewEvent는 외부 시스템과 연결되는 사이드 이펙트다.
이렇게 분리하면 "어디까지가 계산이고, 어디부터가 외부 세계와의 연결인가"가 명확해진다.
일급 객체로서의 함수
영상에서 중요한 개념으로 언급되는 것 중 하나가 함수를 일급 객체로 다룰 수 있다는 점이다.
일급 객체라는 말은 어렵게 들리지만, 쉽게 말하면 함수를 값처럼 다룰 수 있다는 뜻이다.
JavaScript에서 함수는 다음이 가능하다.
- 변수에 담을 수 있다.
- 함수의 인자로 넘길 수 있다.
- 함수의 반환값으로 돌려줄 수 있다.
- 배열이나 객체 안에 넣을 수 있다.
예를 들어 다음 코드를 보자.
const isAdult = (user: User) => user.age >= 20;
const adults = users.filter(isAdult);여기서 isAdult는 함수지만, filter의 인자로 전달되고 있다.
함수를 값처럼 넘기고 있는 것이다.
React에서도 이 개념은 아주 자연스럽게 사용된다.
<Button onClick={() => navigate('/home')}>홈으로 이동</Button>onClick에 함수를 넘길 수 있는 이유도 JavaScript에서 함수가 일급 객체이기 때문이다.
커스텀 훅에서도 자주 활용된다.
function useConfirmAction(action: () => void) {
const confirmAndRun = () => {
if (window.confirm('정말 실행할까요?')) {
action();
}
};
return confirmAndRun;
}이 훅은 어떤 액션이 들어오든 "확인 후 실행"이라는 공통 흐름을 재사용할 수 있다.
const deleteWithConfirm = useConfirmAction(deletePost);
const submitWithConfirm = useConfirmAction(submitForm);즉, 함수를 일급 객체로 다룰 수 있다는 것은 단순히 문법적인 특징이 아니다.
프론트엔드에서 이벤트 처리, 훅 설계, 콜백 전달, 로직 재사용을 가능하게 하는 중요한 기반이다.
파이프라이닝: 데이터 흐름을 읽기 쉽게 만들기
함수형 프로그래밍에서 자주 나오는 또 다른 개념은 파이프라이닝이다.
파이프라이닝은 데이터를 여러 함수에 순서대로 통과시키는 방식이다.
- 데이터
- -> A 함수
- -> B 함수
- -> C 함수
- -> 결과
프론트엔드에서는 API 응답을 화면에 맞는 데이터로 바꿀 때 자주 사용된다.
const visibleProducts = products
.filter(isAvailable)
.filter(matchesKeyword(keyword))
.map(toProductCard)
.sort(byPriceAsc);이 코드는 단순히 짧아서 좋은 게 아니다.
데이터가 어떤 과정을 거쳐 화면에 표시되는지 순서대로 읽을 수 있다.
- 판매 가능한 상품만 남긴다.
- 검색어에 맞는 상품만 남긴다.
- 카드 UI에 필요한 형태로 바꾼다.
- 가격 오름차순으로 정렬한다.
영상에서도 파이프라이닝된 코드를 보며 "가독성이 좋다"는 인상을 받았다고 말한다.
프론트엔드에서는 "서버 데이터"와 "화면 데이터"가 항상 같지 않다.
서버 응답을 그대로 렌더링하기보다, UI에 맞는 형태로 가공해야 하는 경우가 많다. 이때 함수형 스타일은 데이터 변환 과정을 명확하게 만든다.
Lazy Evaluation: 정말 필요할 때 실행하기
모닥불 영상에서 Lazy Evaluation도 언급된다.
지연 평가는 쉽게 말하면 값을 미리 계산하지 않고, 실제로 필요할 때 계산하는 방식이다.
예를 들어 다음 코드는 즉시 실행된다.
const result = expensiveCalculation();반면 다음 코드는 실행을 미룬다.
const getResult = () => expensiveCalculation();expensiveCalculation은 getResult()를 호출하기 전까지 실행되지 않는다.
React에서도 비슷한 사고를 볼 수 있다.
const [value, setValue] = useState(() => {
return getExpensiveInitialValue();
});초기값을 함수로 넘기면, 초기 렌더링에 필요한 시점에만 계산된다.
Lazy Evaluation을 꼭 고급 개념으로만 볼 필요는 없다.
실무적으로는 실행 시점을 의식하는 습관에 가깝다.
- 이 계산은 지금 꼭 필요한가?
- 사용자가 특정 UI를 열었을 때 실행해도 되는가?
- 초기 렌더링 전에 반드시 처리해야 하는가?
이런 질문을 던질 수 있다면, 이미 지연 평가의 실무적 가치를 활용하고 있는 것이다.
객체지향 프로그래밍이란 무엇인가?
그렇다면 객체지향 프로그래밍, 즉 OOP는 무엇일까?
객체지향 프로그래밍은 쉽게 말하면 상태와 행위를 가진 객체들이 서로 협력하도록 프로그램을 구성하는 방식이다.
예를 들어 User라는 객체가 있다고 해보자.
class User {
constructor(
private name: string,
private age: number,
) {}
isAdult() {
return this.age >= 20;
}
}여기서 User는 name, age라는 상태를 가지고 있고, isAdult라는 행위를 가진다.
객체지향의 핵심은 단순히 class를 쓰는 것이 아니다.
중요한 것은 책임을 가진 단위들이 서로 협력하도록 구조를 설계하는 것이다.
모닥불 영상에서도 객체지향을 클래스 컴포넌트와 동일하게 보는 것은 오해라고 말한다.
객체지향에서 중요한 것은 클래스가 아니라 객체이고, 객체는 상태와 책임을 가진 단위로 이해할 수 있다.
프론트엔드에서 객체지향은 어떻게 적용될까?
프론트엔드에서는 객체지향을 꼭 class 문법으로만 생각할 필요가 없다.
컴포넌트, 훅, 모듈도 각각 책임을 가진 단위로 볼 수 있다.
- 컴포넌트 = UI 렌더링 책임을 가진 단위
- 훅 = 상태와 로직을 다루는 책임을 가진 단위
- API 모듈 = 서버 통신 책임을 가진 단위
- 도메인 모듈 = 특정 비즈니스 규칙을 다루는 단위
예를 들어 게시글 페이지가 있다고 해보자.
function PostPage() {
const { posts } = usePosts();
const filteredPosts = usePostFilter(posts);
return <PostList posts={filteredPosts} />;
}이 구조에서는 책임이 나뉘어 있다.
- PostPage: 페이지 조립
- usePosts: 게시글 조회
- usePostFilter: 게시글 필터링
- PostList: 게시글 목록 렌더링
이런 책임 분리는 객체지향의 중요한 사고방식과 연결된다.
객체지향의 대표적인 원칙 중 하나인 단일 책임 원칙도 프론트엔드에 그대로 적용할 수 있다.
나쁜 예시는 이런 형태다.
function PostPage() {
// API 요청
// 로딩 처리
// 에러 처리
// 검색어 상태
// 필터링
// 정렬
// 모달 상태
// 렌더링
}하나의 컴포넌트가 너무 많은 책임을 가진다.
반면 책임을 나누면 구조가 명확해진다.
function PostPage() {
const postQuery = usePosts();
const filterState = usePostFilterState();
const posts = useFilteredPosts(postQuery.data, filterState);
return (
<PostPageLayout>
<PostFilter {...filterState} />
<PostList posts={posts} />
</PostPageLayout>
);
}이렇게 하면 각 단위가 무엇을 책임지는지 분명해진다.
함수 컴포넌트는 함수형 프로그래밍일까?
React에서는 함수 컴포넌트 (Function Components)를 사용한다.
그래서 함수 컴포넌트는 곧 함수형 프로그래밍이라고 생각하기 쉽다.
하지만 모닥불 영상에서는 이 지점을 조심해야 한다고 말한다.
함수 컴포넌트라고 해서 반드시 함수형 프로그래밍인 것은 아니다.
예를 들어 다음 컴포넌트를 보자.
function UserProfile() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser().then(setUser);
}, []);
return <div>{user?.name}</div>;
}이 컴포넌트는 함수 형태로 작성되어 있지만, 내부에는 상태와 사이드 이펙트가 있다.
- state
- effect
- API 요청
- 렌더링
순수 함수처럼 입력을 받아 출력만 반환하는 구조와는 다르다.
따라서 중요한 것은 함수 컴포넌트냐 클래스 컴포넌트냐가 아니다.
더 중요한 질문은 다음과 같다.
- 이 컴포넌트의 책임은 명확한가?
- 상태는 적절한 위치에 있는가?
- 사이드 이펙트는 관리 가능한 곳에 있는가?
- 순수 로직과 외부 효과가 분리되어 있는가?
함수형과 객체지향은 대립 관계가 아니다
영상에서 가장 중요한 메시지는 이것이다.
함수형 프로그래밍과 객체지향 프로그래밍은 대립 관계가 아니다.
객체지향은 현실의 무언가를 객체로 관찰하고, 그 객체들의 상태와 책임, 협력 관계를 코드로 표현하는 방식에 가깝다.
반면 함수형 프로그래밍은 추상적인 함수들을 조합해서 입력에서 출력으로 이어지는 흐름을 만드는 방식에 가깝다.
정리하면 다음과 같다.
| 관점 | 객체지향 프로그래밍 | 함수형 프로그래밍 |
|---|---|---|
| 중심 개념 | 객체, 상태, 책임, 협력 | 함수, 입력, 출력, 조합 |
| 강점 | 구조 설계, 모듈 분리, 책임 분리 | 데이터 변환, 순수 로직, 사이드 이펙트 제어 |
| 프론트엔드 예시 | 컴포넌트 책임 분리, 훅 역할 분리, 도메인 모듈 설계 | map, filter, reduce, 순수 함수, 파이프라인 |
| 주로 도움 되는 범위 | 거시적 설계 | 미시적 구현 |
영상에서도 한쪽은 객체지향을 구조 설계나 모듈 분리 같은 거시적 관점에서 유용하다고 보고, 함수형 프로그래밍은 코드의 가독성이나 사이드 이펙트 제어 같은 미시적 관점에서 유용하다고 말한다.
이 관점은 프론트엔드 개발에 특히 잘 맞는다.
예를 들어 프로젝트 구조는 객체지향적으로 책임을 나눌 수 있다.
features/post
├─ api
├─ model
├─ ui
└─ hooks반면 내부 데이터 처리 로직은 함수형으로 작성할 수 있다.
const postCards = posts.filter(isVisiblePost).map(toPostCard).sort(byLatest);즉, 실무에서는 이렇게 말할 수 있다.
거시적으로는 객체지향적으로 책임을 나누고,
미시적으로는 함수형으로 로직을 조합한다.
결국 중요한 것은 사용자 가치다
모닥불 영상 후반부에서는 프로그래밍 패러다임보다 더 본질적인 이야기도 나온다.
개발자는 코드를 작성하는 사람이지만, 기업에서 일하는 개발자라면 결국 사용자의 문제를 해결하고 제품의 가치를 만들어야 한다.
아무리 아름다운 코드라도 사용자에게 전달되지 못하거나, 제품에 긍정적인 영향을 주지 못한다면 그 가치는 제한적이다.
즉, 함수형 프로그래밍과 객체지향 프로그래밍은 모두 목적이 아니라 수단이다.
중요한 질문은 이것이다.
- 이 구조가 변경에 강한가?
- 이 로직이 예측 가능한가?
- 이 코드가 팀원이 이해하기 쉬운가?
- 이 설계가 사용자 경험을 더 안정적으로 만드는가?
- 이 개선이 제품의 가치로 이어지는가?
함수형 프로그래밍을 공부하는 이유도 "멋있어 보여서"가 아니라, 복잡한 로직을 더 예측 가능하게 만들기 위해서다.
객체지향을 공부하는 이유도 단순히 클래스를 잘 쓰기 위해서가 아니라, 책임을 명확히 나누고 변경에 강한 구조를 만들기 위해서다.
내가 생각하는 프론트엔드에서의 활용 방식
프론트엔드 개발에서는 함수형 프로그래밍과 객체지향 프로그래밍을 다음처럼 활용하는 것이 현실적이라고 생각한다.
객체지향적으로 볼 부분
- 컴포넌트의 책임
- 훅의 역할
- 도메인 모듈 분리
- 상태의 소유권
- 레이어 구조
- API와 UI의 경계
예를 들어 하나의 컴포넌트에 모든 책임을 몰아넣기보다, 페이지, UI 컴포넌트, 훅, API 모듈로 나누는 것이 좋다.
- PostPage: 페이지 조립
- PostList: 목록 렌더링
- usePosts: 서버 상태 조회
- usePostFilter: 필터 상태 관리
- postApi: 서버 통신
함수형으로 볼 부분
- 데이터 변환
- 필터링
- 정렬
- 검증
- 포맷팅
- 이벤트 payload 생성
- 상태 업데이트 로직
예를 들어 서버 응답을 화면 모델로 바꾸는 로직은 함수형으로 분리하면 좋다.
function toPostCard(post: Post): PostCard {
return {
id: post.id,
title: post.title,
description: post.content.slice(0, 100),
createdAt: formatDate(post.createdAt),
};
}그리고 조합할 수 있다.
const postCards = posts.filter(isPublicPost).map(toPostCard).sort(byLatest);이렇게 하면 테스트도 쉬워진다.
expect(toPostCard(post)).toEqual({
id: 1,
title: '함수형 프로그래밍',
description: '프론트엔드에서...',
createdAt: '2026.05.01',
});결론
함수형 프로그래밍은 프론트엔드 개발에 분명 도움이 된다.
특히 JavaScript에서는 함수가 일급 객체이기 때문에 콜백, 고차 함수, 훅, 이벤트 핸들러, 데이터 변환 로직에서 함수형 사고를 자연스럽게 활용할 수 있다. 순수 함수와 사이드 이펙트를 구분하면 테스트하기 쉽고 예측 가능한 코드를 만들 수 있으며, 파이프라이닝을 활용하면 데이터 흐름도 훨씬 명확해진다.
하지만 함수형 프로그래밍이 객체지향보다 우월하다고 보기는 어렵다.
객체지향은 컴포넌트와 훅의 책임 분리, 모듈 구조, 상태의 소유권 같은 거시적 설계에 유용하다. 함수형 프로그래밍은 내부 로직을 작고 명확하게 만드는 미시적 구현에 유용하다.
결국 중요한 것은 특정 패러다임을 따르는 것이 아니다.
프론트엔드 개발자는 상황에 맞게 사고방식을 선택해야 한다.
구조를 설계할 때는 객체지향적으로 책임을 나누고,
로직을 작성할 때는 함수형으로 예측 가능하게 만든다.
이 관점이 함수형 프로그래밍과 객체지향 프로그래밍을 프론트엔드 개발에서 가장 현실적으로 활용하는 방법이라고 생각한다.