이 글을 읽기 전에 이 시리즈의 이전 글 ‘생산성 높은 리액트 프로젝트: 어니언 아키텍처’를 먼저 읽어보시길 권해드립니다.

프리젠터는 일반적으로 상태를 포함하지 않은 리액트 컴포넌트를 의미합니다. 즉 useStateuseRef 등을 포함한 모든 상태 관리자가 없는 컴포넌트가 프리젠터입니다. 이러한 프리젠터는 입력받는 props에 따라서만 결과가 달라지고 부수효과를 전혀 발생시키지 않기 때문에 순수 함수입니다. 그래서 어니언 아키텍처를 이용해 리액트 프로젝트를 구조화할 때, 인터랙션 계층에도 순수 함수가 포함될 수 있게 됩니다.

하지만 순수 함수로서의 리액트 컴포넌트는 예상과는 다르게 작성하기 쉽지 않습니다. 기본적인 입력 컴포넌트가 순수 함수가 아닐 가능성이 매우 높기 때문입니다. 예를 들어, 다음과 같이 입력받은 문자 중 숫자를 모두 제거하는 단순한 리액트 컴포넌트를 살펴봅시다.

const WordInput = (props: {
  value?: string;
  onChange?: (value: string) => void;
}) => {
  const { value, onChange } = props;

  const removeNumber = (rawValue?: string) =>
    rawValue?.replace(/\d+/g, "") ?? "";

  const inputRef = useRef<HTMLInputElement>(null);

  const setInputValue = (value: string) => {
    if (inputRef.current) inputRef.current.value = value;
  };

  if (inputRef.current?.value !== removeNumber(value)) {
    setInputValue(removeNumber(value));
  }

  return (
    <input
      ref={inputRef}
      defaultValue={removeNumber(value)}
      onChange={({ target }) => {
        const value = removeNumber(target.value);
        setInputValue(value);
        onChange?.(value);
      }}
    />
  );
};

이 컴포넌트는 입력되는 값에서 숫자를 제거하기 위해 useRef를 이용한 내부 상태를 가지고 있습니다. 내부 상태는 분명 ‘암묵적 입력과 출력’(부수효과)을 유발하기 때문에, 내부 상태가 있는 컴포넌트는 순수 함수가 될 수 없습니다. 따라서, 언뜻 보기에는 프리젠터처럼 보일지라도 이 컴포넌트를 사용하는 모든 컴포넌트는 프리젠터가 될 수 없습니다. MUI와 같이 널리 사용되는 디자인 시스템의 컴포넌트도 대부분 내부 상태를 가지고 있습니다. 이러한 디자인 시스템을 사용해 작성된 컴포넌트는 그 컴포넌트 내부에는 상태를 가지고 있지 않더라도, 프리젠터가 될 수 없는 것입니다.

그렇다면 어니언 아키텍처가 도입된 리액트 프로젝트의 인터랙션 계층에서 순수 함수를 분리하기 위한 프리젠터 계층은 불가능한 걸까요? 엄밀히 말하면 그렇습니다. 하지만 프리젠터의 의미를 다음과 같이 조금만 확장하면 ‘준순수 함수’가 될 수 있습니다.

프리젠터: 전역 상태를 포함하지 않는 리액트 컴포넌트

변수가 전역 변수와 지역 변수로 나누어 지듯이, 리액트의 상태도 전역 상태와 지역 상태로 나누어 집니다. Recoil과 같은 클라이언트 상태 관리자, TanStack Query와 같은 서버 상태관리자에 의해 생성되는 상태는 코드의 어느 부분에서도 변경 가능하고 그 영향이 코드 전체로 퍼지기 때문에, ‘전역 상태’입니다. 그에 반해 useStateuseRef에 의해 생성된 상태는 다른 함수나 컴포넌트에서는 접근할 수 없고, 변경에 의한 영향도, 상태가 포함된 컴포넌트로 제한되는 ‘지역 상태’입니다. 지역 상태를 포함한 컴포넌트에는 암묵적 입력과 암묵적 출력이 있지만, 그것으로 인한 단점은 존재하지 않습니다.

어떤 함수에 암묵적 입력과 출력이 있다면 다른 컴포넌트와 강하게 연결된 컴포넌트라고 할 수 있습니다. 다른 곳에서 사용할 수 없기 때문에 모듈이 아닙니다. 이런 함수의 동작은 연결된 부분의 동작에 의존합니다. … 암묵적 입력과 출력이 있는 함수는 아무 때나 실행할 수 없기 때문에 테스트하기 어렵습니다. (에릭 노먼드 <쏙쏙 들어오는 함수형 코딩> 91쪽)

지역 상태를 포함하는 리액트 컴포넌트는 암묵적 입력과 출력을 가지고 있지만, 다른 컴포넌트와 ‘느슨하게’ 연결되어 있고, 재사용할 수 있으며, Testing Library와 같은 테스트 도구를 사용하면 순수 함수를 테스트하는 것 만큼 쉽게 테스트 할 수 있습니다. 지역 상태를 포함하는 것의 결과만 생각한다면 순수 함수와 전혀 차이점이 없는 것입니다. 따라서 전역 상태를 포함하지 않은 컴포넌트를 ‘준순수 함수’라고 부를 수 있을 것입니다.

이제 인터랙션 계층을 다시 컨테이너 계층과 프리젠터 계층으로 나눌 수 있게 되었습니다. 하지만 이는 컨테이너를 전역 상태를 포함한 리액트 컴포넌트로, 프리젠터를 전역 상태를 포함하지 않은 리액트 컴포넌트로 재정의할 때 가능합니다. 리액트 컴포넌트의 수준에서의 함수형 프로그래밍은 순수 함수의 최대화가 아니라 ‘준순수 함수’의 최대화로 그 지향점이 ‘조금’ 변경되어야 합니다.

profile
프론트엔드 개발자

0개의 댓글