Skip to content

React와 함수형 프로그래밍 - 더 나은 코드를 위한 핵심 원칙

Published: at 오후 09:41

우리는 React의 내부 메커니즘, 모범 사례, 디자인 패턴, 그리고 고급 개념들을 탐구합니다.

이 글들은 기본을 넘어 React가 내부적으로 어떻게 작동하는지 진정으로 이해하고자 하는 React 개발자들을 위해 작성되었습니다.

서론: 함수형 프로그래밍이란 무엇인가?

‘함수형 프로그래밍(Functional Programming)‘은 계산을 ‘수학적 함수의 평가’로 취급하고, ‘상태 변경’과 ‘변경 가능한 데이터(mutable data)‘를 피하는 프로그래밍 접근 방식입니다.

이는 많은 개발자에게 더 익숙한 전통적인 ‘명령형 프로그래밍’과는 크게 다릅니다.

마치 누군가에게 식사를 준비하기 위해 상세한 단계별 지침을 주는 것(‘명령형’ 접근 방식)과, 재료와 적용할 변환 과정을 설명하는 레시피를 주는 것(‘함수형’ 접근 방식)의 차이를 생각해 보세요.

함수형 프로그래밍에서 우리는 따라야 할 단계보다는 ‘데이터 변환’에 집중합니다.

다른 패러다임과의 차이점

React에 특화된 함수형 개념에 뛰어들기 전에, 함수형 프로그래밍이 다른 패러다임과 어떻게 다른지 이해해 보겠습니다.

// 명령형 접근 방식
let sum = 0; // 외부 상태
for (let i = 1; i <= 5; i++) {
  sum += i; // 상태를 직접 변경(mutate)합니다.
}
console.log(sum); // 15



// 함수형 접근 방식
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, current) => acc + current, 0); // 새로운 값을 반환합니다.
console.log(sum); // 15



// 객체 지향 접근 방식
class Counter {
  constructor() {
    this.count = 0; // 상태(데이터)를 객체 내부에 캡슐화
  }

  increment() {
    this.count += 1; // 객체 자신의 상태를 변경하는 메서드
    return this.count;
  }
}
const counter = new Counter();
counter.increment(); // 객체의 내부 상태가 변경됨



React는 왜 함수형 프로그래밍을 선택했을까?

React는 몇 가지 중요한 이유로 많은 함수형 프로그래밍 개념을 사용합니다.

React는 순수 함수형 프로그래밍의 모든 측면을 엄격하게 따르지는 않더라도, 이러한 함수형 원칙들을 활용하여 더 예측 가능하고 유지보수하기 쉬운 프로그래밍 모델을 제공합니다.

1. 일급 객체로서의 함수 (Functions as First-Class Values)

JavaScript에서 함수는 ‘일급 객체’입니다.

이는 함수가 언어의 다른 모든 값처럼 취급될 수 있음을 의미합니다.

이 특징은 함수형 프로그래밍의 기본이며 React에서 널리 사용됩니다.

실질적으로 이것은 JavaScript의 함수가 다음과 같은 일을 할 수 있다는 뜻입니다.

이 유연성은 함수가 일급 객체가 아닌 언어에서는 어렵거나 불가능한 디자인 패턴을 가능하게 합니다.

// 함수가 변수에 저장됩니다.
const handleClick = () => {
  console.log("Button clicked");
};

// 함수가 prop으로 컴포넌트에 전달됩니다.
function Button({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}

function App() {
  return <Button onClick={handleClick}>Click here</button>;
}



2. 고차 컴포넌트 (Higher-Order Components, HOC)

HOC는 React에서 일급 함수의 개념을 가장 강력하게 적용한 예시 중 하나입니다.

HOC는 ‘컴포넌트를 인자로 받아, 새로운 기능이 추가된 새 컴포넌트를 반환하는 함수’입니다.

이 기법은 컴포넌트 로직 재사용과 관심사 분리를 가능하게 합니다.

‘Hooks’가 등장하기 전까지, HOC는 로직을 공유하는 가장 일반적인 방법이었습니다.

// 로딩 상태를 추가하는 HOC
function withLoading(Component) {
  // 새 컴포넌트를 반환합니다.
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <div>Loading...</div>;
    }
    // 로딩이 아니면, 원본 컴포넌트를 렌더링합니다.
    return <Component {...props} />;
  };
}

// 기본 컴포넌트
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// 로딩 상태가 추가된 강화된 컴포넌트 생성
const UserListWithLoading = withLoading(UserList);

// 사용 예시
function App() {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  // ... useEffect로 데이터 페칭

  return <UserListWithLoading isLoading={isLoading} users={users} />;
}



HOC는 로직 재사용에 매우 강력하지만, 여러 개를 중첩하면 JSX가 복잡해지는 ‘래퍼 지옥(wrapper hell)‘이나 prop 이름 충돌 같은 단점도 있습니다.

현대 React에서는 대부분의 경우 ‘커스텀 훅’이 더 나은 대안으로 여겨집니다.

3. 순수 함수 (Pure Functions)

순수 함수는 함수형 프로그래밍의 핵심 개념입니다.

순수 함수란:

React에서는 컴포넌트가 그 props에 대해 순수 함수처럼 동작하도록 노력해야 합니다.

동일한 props가 주어지면 항상 동일한 UI를 렌더링해야 하죠.

이 원칙 덕분에 React는 렌더링을 최적화할 수 있습니다.

만약 순수 컴포넌트의 props가 변경되지 않았다면, React는 결과가 동일할 것임을 알기 때문에 렌더링 과정을 건너뛸 수 있습니다.

(React.memo가 바로 이 원리를 이용합니다.

)

// 순수 함수: 오직 매개변수에만 의존합니다.
function calculateTotal(items) {
  return items.reduce((total, item) => total + item.price * item.quantity, 0);
}

// 순수 React 컴포넌트: 동일한 items prop에 대해 항상 동일한 UI를 렌더링합니다.
function OrderSummary({ items }) {
  const total = calculateTotal(items);

  return (
    <div>
      <p>Total: ${total.toFixed(2)}</p>
    </div>
  );
}



4. 불변성 (Immutability)

‘불변성’은 기존 데이터를 직접 수정하지 않고, 대신 원하는 변경 사항이 적용된 ‘새로운 복사본’을 만드는 원칙입니다.

React에서 불변성이 중요한 이유는, React가 변경 사항을 감지하고 DOM을 언제 업데이트할지 결정하는 방식 때문입니다.

React는 객체의 ‘참조(reference)‘를 비교하여 무언가 변경되었는지 판단합니다.

만약 객체를 직접 수정하면(mutation), 객체의 메모리 주소, 즉 참조는 그대로 유지됩니다.

React는 '이전 상태 === 새로운 상태' 인지 매우 빠른 참조 비교 검사를 수행하는데, 참조가 동일하면 아무것도 바뀌지 않았다고 착각하고 리렌더링을 건너뛰게 됩니다.

불변성을 지키는 방법 (해야 할 것)

function TodoList() {
  const [todos, setTodos] = useState([
    /* ... */
  ]);

  const toggleTodo = id => {
    // map을 사용해 '새로운 배열'을 생성합니다.
    setTodos(
      todos.map(todo =>
        todo.id === id
          ? { ...todo, completed: !todo.completed } // 스프레드 연산자로 '새로운 객체'를 생성합니다.
          : todo
      )
    );
  };
  // ...
}



직접 수정하는 방법 (피해야 할 것)

function TodoListBad() {
  const [todos, setTodos] = useState([
    /* ... */
  ]);

  // ❌ 나쁜 접근 방식 (직접 수정)
  const toggleTodoBad = id => {
    const todoIndex = todos.findIndex(todo => todo.id === id);
    // ❌ 기존 배열 내부의 객체를 직접 수정합니다.
    todos[todoIndex].completed = !todos[todoIndex].completed;
    // 배열의 참조가 바뀌지 않았기 때문에 React는 변화를 감지하지 못합니다.
    setTodos(todos);
  };
  // ...
}



5. 커링과 합성 (Currying & Composition)

‘커링’은 여러 인자를 받는 함수를, 각각 하나의 인자를 받는 함수들의 연속으로 변환하는 기법입니다.

이를 통해 더 일반적인 함수로부터 특화된 함수를 만들 수 있습니다.

React에서는 특히 여러 데이터를 필요로 하는 이벤트 핸들러를 만들 때 유용합니다.

function ProductList({ products, onProductAction }) {
  // 커링된 함수: action => productId => event
  const handleProductAction = action => productId => event => {
    onProductAction(action, productId);
  };

  return (
    <div>
      {products.map(product => (
        <button onClick={handleProductAction("view")(product.id)}>View</button>
      ))}
    </div>
  );
}



‘합성’은 단순한 함수들을 결합하여 더 복잡한 함수를 만드는 원리입니다.

React에서는 작은 컴포넌트들을 조립하여 복잡한 UI를 만드는 방식으로 나타납니다.

상속보다 합성을 사용하는 것이 React에서는 강력히 권장되는 패턴이며, 이는 고도로 재사용 가능하고 모듈적인 컴포넌트를 만들 수 있게 해줍니다.

function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
}

function App() {
  return (
    <Card title="User Profile">
      <UserInfo user={user} />
    </Card>
  );
}



결론

함수형 프로그래밍은 React를 위한 코딩 스타일 이상입니다.

그것은 React의 설계와 아키텍처를 안내하는 철학입니다.

일급 함수, 순수 함수, 불변성, 커링, 합성 같은 원칙들을 채택함으로써, 우리는 더 예측 가능하고, 테스트하기 쉬우며, 유지보수하기 좋은 React 애플리케이션을 만들 수 있습니다.

이 접근 방식은 우리의 코드 작성 방식뿐만 아니라, 해결책을 설계하는 방식까지 변화시킵니다.

그것은 우리를 더 작고, 더 집중되고, 더 재사용 가능한 컴포넌트를 만들도록 밀어붙이며, 이는 결국 유지보수하고 발전시키기 더 쉬운 애플리케이션으로 이어집니다.