리액트에서 함수형 프로그래밍을 지향하는 이유
함수형 프로그래밍과 객체 지향 프로그래밍의 장단점, 리액트에서 함수형을 선택한 이유에 대한 탐구에 대한 기록
댓글
입력한 비밀번호는 비밀 댓글 열람, 수정, 삭제에 사용됩니다.
함수형 프로그래밍과 객체 지향 프로그래밍의 장단점, 리액트에서 함수형을 선택한 이유에 대한 탐구에 대한 기록
입력한 비밀번호는 비밀 댓글 열람, 수정, 삭제에 사용됩니다.
순수함수 기반으로 데이터 흐름을 만들 경우, 성능보다 복잡성이 커지는 경우도 있음
| 구분 | 프론트엔드 (React 등) | 백엔드 (Node.js, Java 등) |
|---|---|---|
| 주요 고려 요소 | UI 렌더링 최적화, 상태 변화 추적 | 성능, 리소스 관리, 트랜잭션 흐름 |
| 적합한 패러다임 | 함수형 프로그래밍 | 객체지향 프로그래밍 |
| 장점 | 테스트 용이, 불변성으로 상태 변화 추적, 컴포넌트 재사용성 | 클래스 기반 구조로 책임 명확화, 상태 유지, 연결 중심의 설계, 복잡한 로직 구조화 용이 |
| 단점 | 복잡한 연산 흐름에선 불편, 무분별한 hook 남용 시 가독성 저하 | 테스트 시 상태 분리 어려움, 리렌더링 최적화 구조는 복잡함 |
리액트(React)는 최근 몇 년간 함수형 프로그래밍(Functional Programming, FP) 을 지향하게 되었습니다.
단순한 UI 컴포넌트에서부터 복잡한 어플리케이션 구조까지 함수형 접근법의 장점을 적극적으로 활용합니다.
이번 글에서는 리액트에서 함수형 프로그래밍을 지향하는 이유와 각각의 근거를 체계적으로 정리해보겠습니다.
함수형 프로그래밍의 핵심은 '순수 함수(pure function)'입니다. 순수 함수란 입력이 같으면 항상 같은 출력을 내고, 외부 상태를 변경하지 않는 함수를 말합니다.
클래스형 컴포넌트의 테스트:
함수형 컴포넌트:
함수형 컴포넌트는 로직과 렌더링을 분리할 수 있어서 각각을 독립적으로 테스트할 수 있습니다.
순수 함수 예시:
함수형 프로그래밍에서 데이터는 불변(immutable)하게 다루는 것이 기본입니다.
불변성을 지키면 리액트의 최적화 도구들이 매우 효과적으로 동작합니다:
React.memo의 효율적인 동작:
useMemo와 useCallback의 정확한 의존성 감지:
잘못된 방식 vs 올바른 방식:
불변성을 지키면 참조 비교(===)만으로 상태 변화 감지가 가능해집니다.
깊은 비교 vs 참조 비교:
리액트는 상태가 변했는지 확인할 때 Object.is() (거의 ===와 동일)를 사용합니다:
이처럼 불변성을 유지하면, 복잡한 데이터 구조에서도 변화 감지가 O(1) 시간에 이루어져 성능상 큰 이점을 얻을 수 있습니다.
**메모이제이션(Memoization)**은 같은 입력에 대해 계산을 반복하지 않고 캐시된 값을 반환하는 최적화 기법입니다. 함수형 프로그래밍은 **순수 함수(pure function)**를 전제로 하기에, 메모이제이션이 매우 효과적입니다.
, 같은 훅도 이 개념에 기반순수하지 않은 함수는 메모이제이션하기 어려움:
이처럼 함수형의 "불변성 + 순수성"은 메모이제이션의 전제조건과 일치하기 때문에, 두 개념은 서로를 강화해줍니다.
함수형 컴포넌트와 Hook(useState, useReducer)을 활용하면 상태 관리 코드가 간결해집니다.
이렇게 하면 UI 렌더링 로직과 데이터 로직을 명확히 분리할 수 있어 가독성이 높아집니다.
리액트가 함수형 프로그래밍을 채택하는 이유는 코드의 명확성, 테스트 용이성, 최적화와 변화 감지의 효율성 때문입니다.
함수형 패러다임은 컴포넌트를 모듈화하고, 복잡도를 제어하는 데 효과적인 전략입니다.
저의 주관적인 판단입니다
프론트엔드(React) 는 UI 렌더링, 상태 변화 감지, 최적화, 테스트 등을 효율적으로 하기 위해 함수형 패러다임이 적합한 것 같습니다
반면, 백엔드(Node.js, Java 등) 에서는 성능 이슈나 리소스 관리, 프로세스 흐름 제어 등에서 객체지향(OOP) 이 실용적이라고 생각합니다.
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');// 불변성을 지킬 때
const TodoItem = React.memo(({ todo, onToggle }) => (
<div onClick={() => onToggle(todo.id)}>
{todo.text} {todo.completed ? '✓' : '○'}
</div>
));
// props가 참조상 같으면 리렌더링 안 됨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>;
}// ❌ 직접 수정 - 최적화 도구들이 변화를 감지하지 못함
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
));
}// ❌ 깊은 비교 - 모든 속성을 재귀적으로 확인 (느림)
function deepEqual(obj1, obj2) {
// 객체의 모든 key-value를 비교해야 함
return JSON.stringify(obj1) === JSON.stringify(obj2);
}
// ✅ 참조 비교 - 메모리 주소만 확인 (빠름)
function shallowEqual(obj1, obj2) {
return obj1 === obj2; // 한 번의 비교로 끝
}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>
);
}useCallbackfunction 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);
}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>;
}아이디어 선정부터 시니어 피드백을 거쳐 MVP 구현까지, 그룹 프로젝트의 기틀을 다지며 겪은 시행착오와 기술적 도전의 기록
벡터의 길이를 1로 만드는 정규화의 본질을 게임 이동 로직과 블렌더의 Apply Scale 사례를 통해 분석했습니다.
6주간의 그룹 스프린트를 돌아보며 경험한 설계의 시행착오, 협업의 기술, 그리고 시니어 피드백을 통한 성장을 정리한 기록
10주 동안 진행된 부스트캠프 멤버십 학습 스프린트를 돌아보며 기술 학습, 설계 고민, 번아웃, 그리고 AI 활용까지 정리한 기록
네이버 부스트캠프 웹·모바일 10기 챌린지 과정 - 데일리 미션, CS 공부, 피어 피드백, AI 활용 방안
함수형 프로그래밍과 객체 지향 프로그래밍의 장단점, 리액트에서 함수형을 선택한 이유에 대한 탐구에 대한 기록
네이버 부스트캠프 웹·모바일 10기 베이직 과정 지원 준비부터 수료, 문제 해결력 테스트 후기까지 정리한 기록
OSCCA 마스터 기간 동안 Githru 프로젝트에서 진행한 UI 개선, 이슈 해결, Pull Request 경험을 정리한 기록
Open Source Contribution Academy(OSCCA) 챌린지 기간 동안 Githru 프로젝트에 참여하며 이슈 제기와 첫 Pull Request를 경험한 기록
Open Source Contribution Academy에 지원하며 Githru VSCode Extension을 직접 설치하고 분석한 경험 정리
마지막 아티클까지 모두 확인했습니다.