javascript · high priority
함수형 프로그래밍 — 순수함수, 불변성, 합성
React/Redux 의 사상적 토대 — 왜 setState 는 직접 수정하면 안 되고, 왜 useEffect 는 순수해야 하는가
학습 개요
탄생 배경
쉬운 설명
복잡한 개념을 실생활 비유로 설명합니다.
“레시피 카드 vs 즉흥 요리”
순수함수는 정확히 같은 재료에 항상 같은 요리가 나오는 레시피 카드입니다. 누가 어디서 따라해도 결과가 같으니 책으로 출판해도 안전합니다. 비순수함수는 "냉장고에 뭐 있는지 보고 알아서" 같은 즉흥 요리 — 어제와 오늘 결과가 다릅니다. 불변성은 "조리 도중 원본 재료를 칼로 썰지 않고, 새 그릇에 옮겨 담아 자르는" 약속입니다. 그래야 같은 재료를 다른 사람이 동시에 다른 요리에 쓸 수 있죠.
핵심 개념
순수함수는 두 조건을 만족해야 합니다. **동일 입력 → 동일 출력**(referential transparency) 과 **관찰 가능한 부수효과 없음**(no observable side effects). 이 둘을 만족하면 함수는 "수학적 함수" 가 되어 캐싱, 병렬 실행, 안전한 재실행이 모두 가능해집니다 — React 가 컴포넌트를 동시 모드에서 두 번 호출해도 안전한 이유입니다.
1// ❌ 비순수: 외부 상태 변이 + 시간 의존2let total = 0;3function addToTotal(price: number) {4 total += price; // 외부 상태 변경5 return total;6}7addToTotal(100); // 1008addToTotal(100); // 200 — 같은 입력인데 다른 출력 ❌910// ✅ 순수: 인자만 보고 결정, 외부에 손대지 않음11function add(a: number, b: number) {12 return a + b;13}14add(100, 0); // 항상 100
관찰 가능한 부수효과 (observable side effect)
함수 외부에서 결과를 알아챌 수 있는 변화 — 전역/외부 변수 수정, DOM 변경, 네트워크 호출, console.log, 파일 쓰기, Date.now()/Math.random() 사용 등.
참조 투명성 (referential transparency)
f(x) 라는 호출을 그 결과 값으로 그대로 치환해도 프로그램 의미가 변하지 않는 성질. 순수함수는 늘 참조 투명.
`add(2, 3)` 를 본문 어디서 만나도 `5` 로 치환 가능 → 안전하게 메모이즈결정성 (determinism)
같은 입력엔 같은 출력. Math.random, Date.now, 비동기 fetch 처럼 외부 시간/난수에 의존하면 깨짐.
React 컴포넌트는 순수함수여야 한다
React 공식 문서가 명시합니다 — 컴포넌트와 Hook 은 idempotent 해야 하고, 같은 input(props/state/context) 에는 같은 결과를 반환해야 합니다. 그래야 Strict Mode 가 안전하게 두 번 호출할 수 있고, Concurrent Renderer 가 렌더를 중단/재시작할 수 있고, 서버에서 한 컴포넌트가 여러 요청을 처리할 수 있고, props 가 같으면 렌더를 건너뛰는 최적화가 안전해집니다.
1let counter = 0;23function MyComponent() {4 counter++;5 return <div>{counter}</div>;6}78// React Strict Mode 개발 환경에서9// <MyComponent /> 를 한 번 렌더하면 화면에는 무엇이 보일까?
정답
2 가 보일 가능성이 매우 높음 (Strict Mode 가 순수성을 검증하기 위해 의도적으로 두 번 호출)
해설
React Strict Mode 는 개발 환경에서 컴포넌트와 Hook 을 의도적으로 두 번 호출해 부수효과를 노출시킵니다. counter 처럼 컴포넌트 외부 상태를 변경하는 코드는 두 번째 호출 때 결과가 달라지므로 즉시 들통납니다. 정상이라면 같은 props 에 대해 무조건 같은 JSX 가 나와야 합니다.
흔한 오답
- "Strict Mode 가 버그" 라고 생각하고 끄기 — 실제로는 부수효과를 빨리 발견하라고 일부러 두 번 호출하는 것
- 컴포넌트 본문에서 ref 가 아닌 외부 변수를 변경하는 패턴 사용 — Concurrent 모드에서 무조건 깨짐
실무 적용
어떤 상황에서 사용하는가
폼 입력값 검증 + 서버 전송 + 토스트 알림이 한 함수에 다 들어 있어 단위 테스트가 fetch 모킹부터 시작해야 하는 상황.
어떻게 적용하는가
계산 로직(validate, normalize, formatPayload) 을 순수함수로 떼어내 별도 파일에 두고, 핸들러는 "순수 호출 → 부수효과 호출 → 후처리" 의 얇은 어댑터로 줄인다. 단위 테스트는 순수함수만 fast 하게 다수 케이스로 검증하고, 부수효과는 별도 통합 테스트(MSW + handler 호출)로 한 번씩만 검증.
흔한 실수와 안티패턴
- "순수함수를 위해 모든 걸 함수로" — 한 줄짜리 add 같은 걸 굳이 분리하면 가독성만 망가짐. 분리는 재사용/테스트 가치가 있을 때만
- `Object.freeze` 로만 불변성 강제 — TypeScript 의 `readonly`/`as const` 가 컴파일 타임에 더 강력. 런타임 freeze 는 성능 손해까지 있음
- reducer 안에서 `state.list.push(x)` 후 같은 state 반환 — 같은 참조라 React-Redux 가 변경을 감지 못해 리렌더 안 됨
- spread 한 번으로 깊은 객체가 안전하다고 착각 — 얕은 복사라 중첩 노드는 같은 참조
- currying 을 모든 함수에 강제 적용 — 호출부가 `f(a)(b)(c)` 로 더 어려워짐. 진짜 부분 적용이 필요한 자리에만
면접 질문
답변 방향 힌트
React 의 변경 감지가 어떤 비교 방식을 쓰는지, 그게 불변성과 어떻게 연결되는지.
반드시 언급할 키워드
- React 는 prev === next 참조 비교로 변경 감지
- 직접 mutate 하면 같은 참조가 유지되어 비교가 false → 리렌더 스킵
- 불변성은 "새 참조 만들기" 를 통해 변경을 신호로 만든다
- Redux, React.memo, useMemo, React Query 의 동작 모두 이 가정 위에 있음
- Object.assign({}, state) 나 spread, Immer 의 produce 로 새 참조 생성
예상 꼬리 질문
- 깊은 객체에서 spread 만으로는 왜 안전하지 않은가요?
- React.memo 가 props 의 깊은 비교를 안 하는 이유는?
자기 점검
스크롤 올리지 말고 답해보세요. 순수함수의 두 조건을 말하고, 위반 사례를 두 개 들어보세요.
기대 키워드
자주 하는 오해
"순수함수 = console.log 만 안 쓰면 된다" 로 오해하기 쉽지만, 외부 변수 읽기, Date.now, Math.random, fetch, DOM 접근 모두 비순수입니다.
왜 React 에서 spread 로 한 번 복사한 객체의 깊은 필드를 mutate 하면 변경 감지가 깨지나요?
기대 키워드
자주 하는 오해
`{...state}` 가 깊은 복사인 줄 아는 경우가 흔합니다. spread 는 1단계만 새로 만들고 그 안의 객체/배열은 같은 참조를 그대로 가져옵니다.
"부수효과를 격리한다" 는 말의 의미를 React 코드에서 구체적으로 설명해보세요.
기대 키워드
자주 하는 오해
"부수효과를 없앤다" 와 혼동하기 쉽지만, 함수형의 목표는 부수효과를 끝단에 모으는 것입니다. 비즈니스 로직은 순수함수, 부수효과는 어댑터(useEffect, handler)에 — 가 핵심.
학습 자료
- Keeping Components Pure — ReactReact 가 명시적으로 요구하는 컴포넌트의 순수성과 그 이유 — Strict Mode, Concurrent Renderer, Server Components 와 직접 연결.DocReact 공식 문서
- Components and Hooks must be pure — Reactpure 의 기술적 정의(idempotent, no side effects)와 위반 시 영향, 허용되는 부수효과 자리(event handler, useEffect, server action) 를 공식 명세로 정리.DocReact 공식 문서
- Updating Objects in State — Reactstate 를 mutate 하지 않고 업데이트하는 패턴, spread 의 얕은 복사 함정, Immer 도입 가이드까지.DocReact 공식 문서
- Immer — 변이 문법으로 불변 업데이트Proxy 기반으로 draft 를 변이하는 것처럼 쓰면 새 객체를 반환하는 라이브러리. Redux Toolkit 이 내장으로 사용.CodeImmer 공식
- Functional-Light JavaScriptJavaScript 에 한정한 함수형 사고 입문서. 순수함수, 합성, 커링까지 실용 관점으로 깊이 있게.BlogKyle Simpson