- Published on
- ·
리액트에서의 함수형 프로그래밍
최근에 함수형 프로그래밍에 대한 책을 읽으면서 프론트엔드 개발에 있어 이 패러다임이 어떻게 적용될 수 있는지에 대해 깊이 고민해 보았습니다.
사실, 리액트를 사용하면서 우리는 이미 함수형 프로그래밍의 핵심 원칙 중 하나인 불변성을 지키도록 권장받고 있습니다. 하지만 함수형 프로그래밍은 이런 단편적인 코드들 뿐만 아니라, 컴포넌트 자체에도 적용할 필요가 있습니다. 이상적으로, 리액트 컴포넌트는 순수 함수처럼 설계되어, 동일한 props
가 주어졌을 때 항상 같은 결과를 렌더링해야 합니다. 하지만 실제 애플리케이션에서는 외부 상태 관리와 상호작용이 필수적이므로, 완전한 순수성을 유지하는 것은 실질적으로 불가능합니다.
useState와 useEffect의 사용
useState
와 useEffect
와 같은 상태
, 부수효과
를 다루는 훅을 컴포넌트 내부에서 사용한다면 그 컴포넌트는 순수 컴포넌트라고 말할 수 있을까요? 저와 똑같은 궁금증을 가진 많은 개발자들이 이미 해당 논의를 진행중이었습니다.
Are React function components pure in terms of functional programming?
It’s not a pure component if it uses state. It doesn’t always return the same output for a given input.
React components are functional in the sense that for a given set of props and state (and technically context values) a component always returns the same UI.
여러 의견이 있지만 제가 내린 결론은 함수형 프로그래밍 관점에서는 순수 컴포넌트가 아니고, 리액트 관점에서는 순수 컴포넌트로 취급한다고 생각합니다.
useState
, useEffect
를 사용하는 컴포넌트는 함수형 프로그래밍에서 정의하는 순수 함수의 조건을 전부 불만족합니다. 내부적인 상태가 존재하여 같은 입력 - props에 다른 출력 - 렌더링이 나올 수 있으니 결정론적이지 않습니다. useEffect
는 네트워크 요청을 포함해 외부 세계와 상호작용할 수 있으니 부수효과가 존재하고 이는 함수형 프로그래밍 입장에선 순수 함수가 아니다라는 결론이 나옵니다. 반면 리액트 팀에서 정의하는 순수 함수는 함수형 프로그래밍과는 다른 것으로 보입니다. 공식문서를 보면, 리액트는 "Components and Hooks must be pure"라는 아티클에서 모순적이게도 useState
, useEffect
사용을 포함한 컴포넌트의 순수성을 유지하는 방법을 알려줍니다.
Side effects should not run in render, as React can render components multiple times to create the best possible user experience.
리액트에서 정의하는 순수 컴포넌트의 가이드라인은 다음과 같습니다.
- Idempotent – You always get the same result every time you run it with the same inputs – props, state, context for component inputs; and arguments for hook inputs.
- Has no side effects in render – Code with side effects should run separately from rendering. For example as an event handler – where the user interacts with the UI and causes it to update; or as an Effect – which runs after render.
- Does not mutate non-local values - Components and Hooks should never modify values that aren’t created locally in render.
리액트 팀에선 state
와 props
심지어 context
마저 입력으로 취급하며, 모두 같다면 동일한 JSX를 반환한다고 말합니다. 또한 렌더링 이후에 발생하는 부수효과가 발생하는 것을 허용하고, 이것을 깔끔하게 관리하는 방식으로 useEffect
를 제공합니다. 즉 리액트에서는 순수함수의 정의를 완화하고 효율적인 형태로 다시 정의했습니다. 실제 어플리케이션을 개발하는 환경과 전통적 함수형 프로그래밍의 순수성 사이의 타협점인 것 같습니다.
왜 적용해야 할까요?
함수형 프로그래밍, 순수 함수를 작성하기 위해서는 노력이 필요합니다. 왜 이런 노력을 해가며 순수성을 분리하고 유지해야 할까요?
- 사이드 이펙트 최소화 - 사이드 이펙트는 개발자가 예측하지 못한 방향으로 작동할 수 있습니다. 코드는 서로 독립적으로 동작하게 됩니다.
- 서버 컴포넌트 - 순수 함수로 작성된 컴포넌트는 다른 환경(서버)에서 실행할 수 있습니다. 서버 컴포넌트는 번들 크기를 줄이고 사용자에게 더 빨리 렌더링된 요소를 보여줄 수 있습니다.
- 캐싱 - 순수 컴포넌트는 항상 동일한결과를 반환하므로 캐싱해도 안전합니다.
- 리렌더링 최적화 - 렌더링 중 데이터가 변경되면 순수 컴포넌트의 경우 즉시 계산을 중단하고 렌더링을 다시 시작할 수 있습니다. 순수 컴포넌트가 아니면 렌더링 완료를 기다려야합니다.
서버 컴포넌트
함수형 프로그래밍에서 정의하는 순수 컴포넌트와 리액트 서버 컴포넌트는 내부적으로 state
나 context
를 사용할 수 없다는 점에서 비슷합니다. 다만 서버 컴포넌트를 사용하기 위해선 상위 컴포넌트가 서버 컴포넌트가 되어야 하며, 이는 비즈니스 로직을 상위로 그리고 뷰 로직과 같은 순수한 부분을 하위로 하여 props
전달을 통해 하위 컴포넌트를 순수 컴포넌트로 만들었던 전략이 불가능해졌다고 생각됩니다. 반대로 서버 컴포넌트를 사용하게 되면서 상위 컴포넌트가 순수성을 유지하고, 하위 컴포넌트에서 비즈니스 로직 및 상호작용을 context
를 활용해서 구현하게 됩니다. 하위 클라이언트 컴포넌트에서 상위 서버 컴포넌트로부터 props
를 통해 state
나 reducer
를 전달 받을 수 없기 때문에, flux
패턴을 강요받게 되고 이는 함수형 프로그래밍에서 정의하는 동일 props => 동일 렌더링과는 거리가 멀어집니다. 또한 서버 컴포넌트는 useEffect
와 같은 훅을 사용할 수 없지만 데이터베이스에 직접 접근은 가능한 부수효과가 일부만 제한된 상태입니다. 그리고 eventListener
같은 상호작용은 순수성과 무관하게 서버 컴포넌트에서 사용이 불가능합니다. 즉 순수성은 서버 컴포넌트를 결정하는 기준이 아니고, 순수하지 않은 부분을 순수한 부분과 분리해서 사용해야지만 몇몇 순수한 부분을 서버 컴포넌트로 만들 수 있게 되는 것입니다.
전역 상태의 사용
ContextAPI
, Redux
나 Zustand
와 같은 전역 상태 관리 라이브러리를 사용하는 것은 코드적으론 함수형 프로그래밍을 활용하지만, 전역 상태에 대한 읽기와 쓰기는 구조적으로 컴포넌트의 순수성을 해칩니다. 컴포넌트 내부에서 전역 상태를 읽고 쓰는 것은 함수형 프로그래밍 진영에서 보기엔 전역 변수를 사용하는 암묵적 읽기, 암묵적 쓰기라고 볼 수 있습니다. 그럼 전역 상태는 전역 변수처럼 사용을 지양해야 하는 것일까요?
그렇지 않습니다. 서버 컴포넌트를 사용하기 위해서 context
를 통한 상태관리는 필수적인 요소입니다. 만약 서버 컴포넌트를 활용하여 개발하려고 한다면, 우리는 이제 입력에 대한 기준을 props
뿐만 아니라 state
와 context
로 확장하는 것으로 타협해야 합니다. 만약 서버 컴포넌트를 활용하지 않는다면 합성 컴포넌트 패턴을 활용하여 prop drilling
없이, props
만을 입력으로 인정하는 방식으로 순수 컴포넌트를 유지할 수 있습니다. 물론 이러한 방식을 서버 컴포넌트를 사용하면서 부분적으로 클라이언트 컴포넌트 내부에서 적용하는 것도 당연히 가능합니다.
결론
함수형 프로그래밍의 순수 함수 그리고 리액트 컴포넌트가 순수하다는 개념은 이상적인 상태를 지향하는 것이며, 실제 개발에서는 필요에 따라 유연하게 조정할 필요가 있습니다. 특히 리액트 서버 컴포넌트는 순수해야 한다는 원칙을 따르면서도, 순수 컴포넌트에 대한 정의를 확장하는 역설적인 상황을 만들어내기도 합니다.
결국, 사용하는 기술이나 방법론에 관계없이, 실용성을 희생하지 않는 범위 내에서 개념적 순수성을 최대한 유지하려는 노력이 필요합니다.