리액트(React)는 최근 몇 년간 함수형 프로그래밍(Functional Programming, FP)을 지향하게 되었습니다.
단순한 UI 컴포넌트에서부터 복잡한 어플리케이션 구조까지 함수형 접근법의 장점을 적극적으로 활용합니다.
이번 글에서는 리액트에서 함수형 프로그래밍을 지향하는 이유와 각각의 근거를 체계적으로 정리해보겠습니다.
함수형 프로그래밍의 핵심은 '순수 함수(pure function)'입니다. 순수 함수란 입력이 같으면 항상 같은 출력을 내고, 외부 상태를 변경하지 않는 함수를 말합니다.
클래스형 컴포넌트의 테스트:
class UserProfile extends React.Component {
state = { name: '', loading: false };
async componentDidMount() {
this.setState({ loading: true });
const user = await fetchUser(this.props.userId);
this.setState({ name: user.name, loading: false });
}
render() {
return <div>{this.state.loading ? 'Loading...' : this.state.name}</div>;
}
}
// 테스트 시 lifecycle과 상태를 모두 고려해야 함
함수형 컴포넌트:
function UserProfile({ userId }) {
const { user, loading } = useUser(userId);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
// 테스트 시 props만 확인하면 됨 (Hook은 별도 테스트)
함수형 컴포넌트는 로직과 렌더링을 분리할 수 있어서 각각을 독립적으로 테스트할 수 있습니다.
순수 함수 예시:
function formatPrice(price, currency = 'USD') {
return `${currency} ${price.toFixed(2)}`;
}
// 입력만으로 결과 예측 가능
expect(formatPrice(10.5)).toBe('USD 10.50');
expect(formatPrice(20, 'KRW')).toBe('KRW 20.00');
함수형 프로그래밍에서 데이터는 불변(immutable)하게 다루는 것이 기본입니다.
불변성을 지키면 리액트의 최적화 도구들이 매우 효과적으로 동작합니다:
React.memo의 효율적인 동작:
// 불변성을 지킬 때
const TodoItem = React.memo(({ todo, onToggle }) => (
<div onClick={() => onToggle(todo.id)}>
{todo.text} {todo.completed ? '✓' : '○'}
</div>
));
// props가 참조상 같으면 리렌더링 안 됨
useMemo와 useCallback의 정확한 의존성 감지:
function TodoList({ todos, filter }) {
// todos 배열이 새로 생성될 때만 필터링 재실행
const filteredTodos = useMemo(() =>
todos.filter(todo => todo.category === filter),
[todos, filter]
);
// 의존성이 변하지 않으면 함수 재생성 안 됨
const handleToggle = useCallback((id) =>
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)),
[]
);
return <div>{/* ... */}</div>;
}
잘못된 방식 vs 올바른 방식:
// ❌ 직접 수정 - 최적화 도구들이 변화를 감지하지 못함
function badUpdate() {
const newTodos = todos;
newTodos[0].completed = true;
setTodos(newTodos); // 참조가 같아서 리렌더링 안 됨
}
// ✅ 새로운 객체 생성 - 최적화 도구들이 정확히 동작
function goodUpdate() {
setTodos(prev => prev.map((todo, index) =>
index === 0 ? { ...todo, completed: true } : todo
));
}
불변성을 지키면 참조 비교(===
)만으로 상태 변화 감지가 가능해집니다.
깊은 비교 vs 참조 비교:
// ❌ 깊은 비교 - 모든 속성을 재귀적으로 확인 (느림)
function deepEqual(obj1, obj2) {
// 객체의 모든 key-value를 비교해야 함
return JSON.stringify(obj1) === JSON.stringify(obj2);
}
// ✅ 참조 비교 - 메모리 주소만 확인 (빠름)
function shallowEqual(obj1, obj2) {
return obj1 === obj2; // 한 번의 비교로 끝
}
리액트는 상태가 변했는지 확인할 때 Object.is()
(거의 ===
와 동일)를 사용합니다:
function Counter() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: 'John', age: 25 });
// ✅ 원시값은 자동으로 불변성 보장
const increment = () => setCount(prev => prev + 1);
// ✅ 객체는 새로 생성해야 변화 감지됨
const updateAge = () => setUser(prev => ({ ...prev, age: prev.age + 1 }));
// ❌ 이렇게 하면 변화가 감지되지 않음
const wrongUpdate = () => {
user.age += 1;
setUser(user); // 같은 참조이므로 리렌더링 안 됨
};
}
function DataTable({ items }) {
// items가 참조상 같으면 정렬을 다시 하지 않음
const sortedItems = useMemo(() => {
console.log('정렬 실행!'); // 언제 실행되는지 확인 가능
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
return (
<table>
{sortedItems.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.price}</td>
</tr>
))}
</table>
);
}
이처럼 불변성을 유지하면, 복잡한 데이터 구조에서도 변화 감지가 O(1) 시간에 이루어져 성능상 큰 이점을 얻을 수 있습니다.
메모이제이션(Memoization)은 같은 입력에 대해 계산을 반복하지 않고 캐시된 값을 반환하는 최적화 기법입니다. 함수형 프로그래밍은 순수 함수(pure function)를 전제로 하기에, 메모이제이션이 매우 효과적입니다.
useMemo
, useCallback
같은 훅도 이 개념에 기반function ProductList({ products, searchTerm }) {
// searchTerm이나 products가 바뀌지 않으면 필터링을 다시 하지 않음
const filteredProducts = useMemo(() => {
console.log('필터링 실행!');
return products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [products, searchTerm]);
return <div>{filteredProducts.map(/* ... */)}</div>;
}
순수하지 않은 함수는 메모이제이션하기 어려움:
// ❌ 순수하지 않음 - 외부 상태에 의존
let discount = 0.1;
function calculatePrice(price) {
return price * (1 - discount); // 외부 변수 참조
}
// ✅ 순수함 - 모든 입력이 매개변수로 전달됨
function calculatePrice(price, discount = 0) {
return price * (1 - discount);
}
이처럼 함수형의 "불변성 + 순수성"은 메모이제이션의 전제조건과 일치하기 때문에, 두 개념은 서로를 강화해줍니다.
함수형 컴포넌트와 Hook(useState
, useReducer
)을 활용하면 상태 관리 코드가 간결해집니다.
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, completed: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<div>
<button onClick={() => dispatch({ type: 'ADD_TODO', text: 'New Todo' })}>
Add Todo
</button>
{/* ... */}
</div>
);
}
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// 여러 컴포넌트에서 재사용 가능
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'ko');
return <div>{/* 설정 UI */}</div>;
}
이렇게 하면 UI 렌더링 로직과 데이터 로직을 명확히 분리할 수 있어 가독성이 높아집니다.
리액트가 함수형 프로그래밍을 채택하는 이유는 코드의 명확성, 테스트 용이성, 최적화와 변화 감지의 효율성 때문입니다.
함수형 패러다임은 컴포넌트를 모듈화하고, 복잡도를 제어하는 데 효과적인 전략입니다.
저의 주관적인 판단입니다
프론트엔드(React)는 UI 렌더링, 상태 변화 감지, 최적화, 테스트 등을 효율적으로 하기 위해 함수형 패러다임이 적합한 것 같습니다
반면, 백엔드(Node.js, Java 등)에서는 성능 이슈나 리소스 관리, 프로세스 흐름 제어 등에서 객체지향(OOP)이 실용적이라고 생각합니다.
구분 | 프론트엔드 (React 등) | 백엔드 (Node.js, Java 등) |
---|---|---|
주요 고려 요소 | UI 렌더링 최적화, 상태 변화 추적 | 성능, 리소스 관리, 트랜잭션 흐름 |
적합한 패러다임 | 함수형 프로그래밍 | 객체지향 프로그래밍 |
장점 | - 테스트 용이 - 불변성으로 상태 변화 추적 - 컴포넌트 재사용성 | - 클래스 기반 구조로 책임 명확화 - 상태 유지, 연결 중심의 설계 - 복잡한 로직 구조화 용이 |
단점 | - 복잡한 연산 흐름에선 불편 - 무분별한 hook 남용 시 가독성 저하 | - 테스트 시 상태 분리 어려움 - 리렌더링 최적화 구조는 복잡 |
네, 리액트에서 함수형 프로그래밍을 지향하는 이유와 그 근거를 아주 체계적으로 잘 정리해주셨습니다. 특히 테스트 용이성, 최적화, 변화 감지 효율성, 그리고 KFC Customer Satisfaction Survey - Welcome 상태 관리와 가독성 측면에서의 이점을 명확한 예시와 함께 설명하여 이해도를 높였습니다.