(번역) 리액트 API와 코드 재사용의 진화

sehyun hwang·2023년 6월 17일
31

FE 번역글

목록 보기
21/34
post-thumbnail

리액트 API와 그 이면의 멘탈 모델의 진화에 대해 추적합니다. 그리고 믹스인부터 hooks, RSCs까지 각 과정에서의 트레이드오프에 대해 알아보겠습니다.

원문 : https://frontendmastery.com/posts/the-evolution-of-react-patterns/

리액트는 UI 구축에 대한 사람들의 사고방식을 바꿔놓았습니다. 리액트가 계속 발전하면서 애플리케이션 구축에 관한 생각도 변화하고 있습니다.

무엇이 어떻게 작동하는지, 또는 어떻게 작동해야만 하는지, 그리고 실제 작동하는 방식 간의 차이에서 버그와 성능 이슈가 발생합니다. 이를 극복할 수 있는 핵심은 기술에 대한 명확하고 정확한 멘탈 모델을 탑재하는 것입니다.

소프트웨어 개발은 팀 스포츠이며, 이 팀은 미래의 우리 자신이나 또는 AI 일 수도 있습니다. 어떻게 동작하는지, 그리고 어떻게 코드를 구축하고 공유하는지에 대한 집단적인 이해는 일관적인 시각과 지속 가능한 코드 베이스 구조를 생성하도록 도와주어 불필요한 과정의 반복을 방지합니다.

이 글에서는 리액트와 다양한 코드 재사용 패턴의 진화에 대해 알아보겠습니다. 그리고 그 기저의 멘탈 모델과 트레이드오프도 함께 알아보도록 하겠습니다.

이 글의 끝에 도달할 때쯤 우리는 리액트의 과거, 현재, 그리고 미래를 더 명확하게 파악할 수 있을 것입니다. 레거시 코드 베이스를 자세히 살펴보고, 다른 기술들은 어떤 접근 방식과 트레이드오프를 택했는지 알아보도록 하겠습니다.

리액트 API의 간략한 역사

객체 지향 디자인 패턴이 자바스크립트 생태계에 만연해있던 시점부터 시작해보겠습니다. 이러한 경향은 초기 리액트 API에 반영되었습니다.

믹스인

React.createClass API는 원래 컴포넌트를 생성하기 위한 방법이었습니다. 자바스크립트에서 네이티브 class 구문을 지원하기 전부터 리액트는 자체적으로 클래스를 표현할 수 있었습니다. 믹스인은 코드 재사용을 위한 일반적인 OOP 패턴입니다. 다음은 간단한 예시입니다.

function ShoppingCart() {
  this.items = [];
}
var orderMixin = {
  calculateTotal() {
    // this.items으로 계산 
  }
  // .. 다른 메서드들
}
// ShoppingCart에 orderMixin을 할당 
Object.assign(ShoppingCart.prototype, orderMixin)
var cart = new ShoppingCart()
cart.calculateTotal()

자바스크립트는 다중 상속을 지원하지 않기 때문에 공유하는 동작을 재사용하거나 클래스 확장을 위해서는 믹스인을 사용해야 했습니다.

다시 리액트로 돌아와서, 문제는 createClass로 생성한 컴포넌트 사이에서 어떻게 로직을 공유할 수 있을까 하는 것이었습니다.

믹스인은 일반적으로 많이 사용되는 패턴이라 적당한 아이디어로 보였습니다. 믹스인으로 컴포넌트의 생명 주기에 접근하여 로직, 상태, 그리고 이펙트를 구성할 수 있었습니다.

var SubscriptionMixin = {
  // 여러 믹스인을 getInitialState 결과에 사용할 수 있습니다
  getInitialState: function() {
    return {
      comments: DataSource.getComments()
    };
  },
  // 컴포넌트가 여러 믹스인을 사용할 때 
  // 리액트는 각 믹스인의 생명 주기 메서드를 병합하여 
  // 각각의 메서드가 모두 호출될 수 있도록 합니다
  componentDidMount: function() {
    console.log('do something on mount')
  },
  componentWillUnmount: function() {
    console.log('do something on unmount')
  },
}
// createClass에 객체를 넘겨줍니다 
var CommentList = React.createClass({
  // mixins 속성에 각각을 정의합니다
  mixins: [SubscriptionMixin, AnotherMixin, SomeOtherMixin],
  render: function() {
    // this.state의 comments은 첫번째 믹스인으로부터 왔습니다
    // (!) 여러 믹스인을 사용하면 각각의 상태가 어디서 왔는지 알기 어렵습니다
    var { comments, ...otherStuff } = this.state
    return (
      <div>
        {comments.map(function(comment) {
          return <Comment key={comment.id} comment={comment} />
        })}
      </div>
    )
  }
})

믹스인은 작은 예제에서는 잘 동작하지만, 규모를 확장할 때 몇 가지 단점이 있습니다.

  • 이름 충돌 : 믹스인은 공유된 네임스페이스를 갖습니다. 따라서 여러 믹스인에서 메서드나 상태에 동일한 이름을 사용하면 충돌이 발생합니다.
  • 암시적인 의존성 : 어느 믹스인이 어떠한 기능과 상태를 제공하는지 한눈에 파악하기가 어렵습니다. 이들은 서로 간 상호작용을 위해 공유된 속성 키를 참조하며, 암시적인 의존 관계를 형성합니다.
  • 로컬 추론의 어려움 : 믹스인은 컴포넌트의 추론과 디버깅을 어렵게 합니다. 예를 들어, 여러 믹스인이 getInitialState 결과에 영향을 미칠 수 있고 따라서 추적하기가 어려워집니다.

이러한 문제의 심각성을 느낀 리액트 팀은 "해롭게 여겨지는 믹스인"이라는 글을 게시하여 앞으로 이 패턴을 사용하지 말 것을 권고했습니다.

고차 컴포넌트 (Higher-order component, HOC)

마침내 자바스크립트에 네이티브 class 구문이 도입되었습니다. 리액트팀은 v15.5에서 createClass API를 사용 중단 시키고 네이티브 클래스를 사용하도록 했습니다.

사람들은 여전히 클래스와 생명 주기 관점에서 생각했기 때문에 이 전환 과정에서 멘탈 모델의 큰 변화는 없었습니다. 이제 생명 주기 메서드가 포함된 리액트 Component 클래스를 확장하여 사용할 수 있게 되었습니다.

class MyComponent extends React.Component {
  constructor(props) {
    // 컴포넌트가 DOM에 마운트되기 이전에 실행됩니다
    // super은 부모 컴포넌트 생성자를 참조합니다
    super(props)
    // super을 호출하면  
    // 여기에서 this.state와 this.props를 사용할 수 있습니다 
  }
  // 생명 주기 메서드는 마운트/언마운트와 관련이 있습니다
  componentWillMount() {}
  componentDidMount(){}
  componentWillUnmount() {}
  // 컴포넌트 업데이트 생명 주기 메서드
  // 몇몇은 이제 UNSAFE_라는 접두사가 붙습니다
  componentWillUpdate() {}
  shouldComponentUpdate() {}
  componentWillReceiveProps() {}
  getSnapshotBeforeUpdate() {}
  componentDidUpdate() {}
  // .. 그리고 다른 메서드들
  render() {}
}

믹스인의 문제점을 염두에 두고, 클래스라는 새로운 컴포넌트 작성 방식에서 어떻게 로직과 이펙트를 공유할 수 있을지에 대한 질문이 제기되었습니다.

고차 컴포넌트(HOC)는 초창기에 등장했습니다. 고차 컴포넌트라는 이름은 함수형 프로그래밍의 개념인 고차 함수에서 따왔습니다.

이들은 믹스인을 대체하는 인기 있는 방법으로 자리잡았고, 컴포넌트를 리덕스 스토어에 연결하는 connect 함수와 리액트 라우터의 withRouter와 같은 라이브러리의 API에 적용되었습니다.

// 추가적인 상태, 동작 또는 속성을 가질 수 있는
// 향상된 컴포넌트를 생성할 수 있는 함수 
const EnhancedComponent = myHoc(MyComponent);
// HOC의 단순화된 예제
function myHoc(Component) {
  return class extends React.Component {
    componentDidMount() {
      console.log('do stuff')
    }
    render() {
      // 주입된 몇몇 속성과 함께 기존 컴포넌트를 렌더링
      return <Component {...this.props} extraProps={42} />
    }
  }
}

HOC는 여러 컴포넌트 사이에 공통된 동작을 공유하는 데 유용했습니다. 이를 통해 래핑된 컴포넌트를 분리된 상태로 유지하면서, 재사용할 수 있을 만큼 일반화할 수 있었습니다.

추상화가 강력한 이유는 일단 추상화할 수단을 얻게 되면 모든 곳에 사용하기 때문입니다. 결국, HOC도 믹스인과 유사한 문제에 직면하게 되었습니다.

  • 이름 충돌 : HOC는 래핑된 컴포넌트에 ...this.props를 분해해서 전달하기 때문에 중첩된 HOC가 서로 오버라이드하는 경우 충돌이 발생할 수 있습니다.
  • 정적 타입 설정의 어려움 : 이 시기는 본격적으로 정적 타입 체커가 유행하기 시작할 무렵이었습니다. 여러 HOC가 래핑된 컴포넌트에 새로운 프로퍼티를 주입하면 올바르게 타입을 설정하기가 어려웠습니다.
  • 모호한 데이터 흐름 : 믹스인의 문제점은 "이 상태가 어디에서 오는 거야?" 였습니다. HOC의 문제점은 "이 속성이 어디에서 오는 거야?" 였습니다. 이들은 모듈 단위에서 정적으로 합성되기 때문에 데이터 흐름을 추적하기가 어려웠습니다.

이러한 문제점들 때문에 HOC을 과도하게 사용하면 깊이 중첩되고 복잡한 컴포넌트 계층 구조와 디버깅하기 어려운 성능 문제가 발생했습니다.

렌더 프로퍼티

HOC의 대안으로 렌더 프로퍼티 패턴이 등장했고, React-Motiondownshift와 같은 오픈 소스 API, 그리고 리액트 라우터를 구축한 사람들에 의해 유명해졌습니다.

<Motion style={{ x: 10 }}>
  {interpolatingStyle => <div style={interpolatingStyle} />}
</Motion>

이 아이디어는 컴포넌트의 속성으로 함수를 넘겨주는 것이었습니다. 그다음 내부적으로 해당 함수를 호출하여 데이터와 메서드를 전달하고, 다시 함수로 제어권을 넘겨주어 원하는 렌더링을 계속 합니다.

HOC와 비교하면 컴포지션이 정적으로 모듈 스코프에서 발생하는 것이 아닌, 런타임에 JSX내에서 발생합니다. 출처가 명확했기 때문에 이름 충돌로 인한 문제가 발생하지 않았습니다. 또한 정적 타입 설정이 훨씬 쉬워졌습니다.

한 가지 불편한 점은 데이터 공급자로 사용할 경우 피라미드가 깊게 중첩되어 시각적으로 잘못된 컴포넌트 계층구조를 형성할 수 있다는 점이었습니다.

<UserProvider>
  {user => (
    <UserPreferences user={user}>
      {userPreferences => (
        <Project user={user}>
          {project => (
            <IssueTracker project={project}>
              {issues => (
                <Notification user={user}>
                  {notifications => (
                    <TimeTracker user={user}>
                      {timeData => (
                        <TeamMembers project={project}>
                          {teamMembers => (
                            <RenderThangs renderItem={item => (
                                // 무엇인가를 한다 
                                // 내가 뭘 하고 있었던 거지?
                            )}/>
                          )}
                        </TeamMembers>
                      )}
                    </TimeTracker>
                  )}
                </Notification>
              )}
            </IssueTracker>
          )}
        </Project>
      )}
    </UserPreferences>
  )}
</UserProvider>

이 무렵에는 상태를 관리하는 컴포넌트와 UI를 렌더링하는 컴포넌트 간에 관심사를 분리하는 게 일반적이었습니다.

훅의 등장으로 "container"와 "presentational" 컴포넌트 패턴은 인기가 떨어졌습니다. 하지만 이들이 추후 서버 컴포넌트와 함께 어떻게 재탄생했는지를 살펴보기 위해 언급할 가치가 있습니다.

어쨌든 렌더 프로퍼티는 composable component API를 생성하는 데 여전히 효과적인 패턴입니다.

훅으로의 진입

리액트 16.8이 출시되면서 훅은 공식적으로 로직과 이펙트를 재사용하는 방법이 되었습니다. 이로써 함수 컴포넌트가 컴포넌트를 작성하는 권장 방법으로 굳어졌습니다.

훅을 사용하면 이펙트를 재사용하고 컴포넌트 내에 배치된 로직을 훨씬 직관적으로 구성할 수 있었습니다. 클래스에서는 로직과 이펙트를 캡슐화하고 공유하는 게 훨씬 까다롭고, 다양한 생명 주기 메서드에 걸쳐 조각조각 흩어져있었습니다.

깊게 중첩된 구조를 단순화하고 평탄화할 수 있었습니다. 급증하는 타입스크립트의 인기와 더불어 타입을 설정하기도 쉬웠습니다.

// 위에서의 인위적인 예제를 평탄화
function Example() {
  const user = useUser();
  const userPreferences = useUserPreferences(user);
  const project = useProject(user);
  const issues = useIssueTracker(project);
  const notifications = useNotification(user);
  const timeData = useTimeTracker(user);
  const teamMembers = useTeamMembers(project);
  return (
    <div>
      {/* 무엇인가를 렌더링 */}
    </div>
  );
}

장단점 이해하기

여러 과정을 거쳐오면서 지금까지 많은 이점이 있었고, 클래스와 관련된 미묘한 문제를 해결했습니다. 하지만 몇 가지 단점도 존재했으니 지금부터 자세히 살펴보겠습니다.

클래스와 함수 사이의 단절

컴포넌트의 사용자 관점에서 이 전환으로 인해 변경된 사항은 없습니다. JSX를 계속 같은 방식으로 렌더링했습니다. 그러나 이제 클래스 컴포넌트와 함수 컴포넌트의 패러다임은 분리되었으며, 특히 이 둘을 동시에 배우는 사람들에게는 더욱 그렇습니다.

클래스 컴포넌트는 OOP와 상태 유지 클래스와 연관성이 있습니다. 그리고 함수 컴포넌트에는 순수 함수와 같은 함수형 프로그래밍의 개념과 연관이 있습니다. 각 모델에는 유용한 비유가 있지만 단지 전체 그림을 부분적으로만 파악할 수 있습니다.

클래스 컴포넌트는 변경 가능한 this로부터 상태와 속성을 읽어 들이고, 수명 주기 이벤트에 대한 응답을 고려합니다. 함수 컴포넌트는 클로저를 활용하고 선언적 동기화와 이펙트 관점에서 생각합니다.

컴포넌트가 함수라면 "속성"은 함수 인자이고 함수는 순수하다는 일반적인 비유는 클래스 기반의 멘탈 모델과는 맞지 않습니다.

반대로 함수형 프로그래밍 모델에서 함수를 "순수"하게 유지하면 리액트 컴포넌트의 핵심 요소인 로컬 상태와 이펙트를 완전히 설명할 수 없습니다. 훅 또한 컴포넌트에서 반환되는 것의 일부라고 생각하는 것이 직관적이지 않아서 상태, 이펙트 및 JSX에 대한 선언적 설명을 형성합니다.

리액트의 컴포넌트에 대한 아이디어, 자바스크립트를 이용한 컴포넌트 구현, 그리고 기존에 존재하는 용어를 이용해서 이를 설명하려는 노력은 리액트를 배우는 이들로 하여금 정확한 멘탈 모델 형성을 어렵게 만들었습니다.

이해의 부족은 버그가 많은 코드로 이어집니다. 버그의 주된 원인은 상태를 설정하거나, 데이터를 가져올 때 발생하는 무한 루프 또는 오래된 속성과 상태였습니다. 명령형으로 사고하고 이벤트와 생명 주기에 대응하는 것은 종종 필요하지도 않은 상태와 이펙트를 초래했습니다.

개발자 경험

클래스 컴포넌트는 componentDid, componentWill, shouldComponent의 관점에서 서로 다른 용어 세트를 갖고, 해당 메서드를 인스턴스에 결합합니다.

함수 컴포넌트와 훅은 외부 클래스를 한 겹 제거하여 이를 단순화하고, 오직 렌더 함수에 집중하도록 도와줍니다. 렌더링할 때마다 모든게 새로 생성되기 때문에 각 렌더링 주기 사이에 무언가를 보존할 수 있어야 한다는 사실을 다시 발견했습니다.

클래스에 친숙한 사람들에게는 처음부터 존재했던 리액트에 대한 새로운 관점이 드러났습니다. 리렌더링 사이에 무엇을 보존할지 정의할 수 있도록 useCallbackuseMemo 같은 API가 도입되었습니다.

의존성 배열을 명시적으로 관리하고, 훅 API의 혼란스러운 구문에 더해 매번 객체의 id를 고려하는 것은 일부 개발자에게 더 나쁜 경험으로 느껴졌습니다. 한편 다른 이들에게는 훅이 리액트의 멘탈 모델과 코드를 크게 단순화했습니다.

실험 기능인 React forget은 리액트 컴포넌트를 미리 컴파일하고, 의존성 배열을 수동으로 기억하고 관리할 필요성을 제거함으로써 개발자 경험 향상을 목표로 합니다. 이는 모든 것을 명시적으로 남겨두는 것과 내부적으로 처리하려는 시도 간의 트레이드오프를 강조합니다.

리액트에 상태와 로직 결합하기

많은 리액트 앱은 상태와 뷰를 분리하기 위해 리덕스 또는 MobX같은 상태 관리 라이브러리를 사용합니다. 이는 MVC의 "뷰"라는 리액트의 원래 태그 라인과 일치합니다.

시간이 지남에 따라 글로벌 모놀리식 스토어에서 더 많은 코로케이션(colocation)으로 전환되었으며, 특히 렌더 프로퍼티와 함께 "모든 것은 컴포넌트"라는 개념이 확산되었습니다. 그리고 이러한 인식은 훅으로 전환되면서 더욱 견고했습니다.

"앱 중심"과 "컴포넌트 중심" 모델 사이에는 모두 트레이드오프가 있습니다. 리액트에서 상태를 분리하여 관리하는 것은 리렌더링시에 더 많은 제어권을 주고, 스토어와 컴포넌트 간의 독립된 개발과 UI로부터 로직을 분리하여 실행하고 테스트하는 것을 가능하게 합니다.

반면에 여러 컴포넌트에서 사용할 수 있는 훅의 코로케이션과 구성 가능성은 로컬 추론, 이식성 및 다음에 다룰 기타 이점을 개선할 수 있습니다.

리액트 진화 이면의 원칙

이러한 패턴의 진화로부터 우리는 뭘 배울 수 있을까요? 그리고 우리를 가치있는 트레이드오프로 이끌어 줄 휴리스틱에는 무엇이 있을까요?

  • API보다 사용자 경험

    프레임워크와 라이브러리는 개발자 경험과 최종 사용자 경험을 모두 고려해야합니다. 사용자 경험과 개발자 경험을 맞바꾸는 것은 잘못된 이분법이지만, 때로는 어느 한 쪽이 다른 쪽보다 우선시되는 경우가 있습니다.

    예를 들어, styled-components와 같은 자바스크립트 라이브러리의 런타임 CSS는 동적 스타일이 많을 때 사용하기 좋지만, 최종 사용자 경험에는 바람직하지 않습니다. 이는 균형을 맞춰야 할 스펙트럼입니다. 라이브러리로서의 리액트는 다른 빠른 프레임워크와 비교했을 때 이 스펙트럼에 속합니다.

    리액트 18의 동시성 기능과 RSC는 더 나은 최종 사용자 경험을 추구하기 위한 노력으로 볼 수 있습니다.

    이를 추구하는 것은 컴포넌트를 구현하는 데 사용하는 API와 패턴의 업데이트를 의미합니다. 함수의 "스냅샷" 속성(클로저)을 사용하면 동시 모드에서 올바르게 작동하고, 서버의 비동기 함수는 서버 컴포넌트를 표현하는 좋은 방법입니다.

  • 구현보다 API

    지금까지 다룬 API와 패턴은 컴포넌트의 내부를 구현하는 관점에서 살펴본 것입니다.

    구현 세부 사항은 createClass에서 ES6 클래스 그리고 상태 유지 함수로 발전했지만, 이펙트로 상태를 유지할 수 있는 "컴포넌트"라는 상위 수준의 API 개념은 안정적으로 유지되었습니다.

    return (
      <ImplementedWithMixins>
        <ComponentUsingHOCs>
          <ThisUsesHooks>
            <ServerComponentWoah />
          </ThisUsesHooks>
        </ComponentUsingHOCs>
      </ImplementedWithMixins>
    )
  • 올바른 기본 요소에 집중

    즉, 견고한 토대를 기반으로 구축해야합니다. 리액트에서 컴포넌트 모델은 선언적으로 사고하고, 로컬에서 추론할 수 있게 해줍니다.

    이를 통해 휴대성이 커지고, 코드 간의 숨겨진 연결을 실수로 끊어내지 않고도 코드를 더 쉽게 삭제, 이동, 복사 및 붙여넣기할 수 있습니다.

    이 모델의 결에 맞는 아키텍처와 패턴은 더 나은 구성 가능성을 제공하지만, 컴포넌트가 관심사의 공존을 포착하고 그에 따른 트레이드오프를 수용해야 하는 경우 로컬 상태를 유지해야 하는 경우가 많습니다.

    이 모델의 세부 사항에 어긋나는 추상화는 데이터 흐름을 모호하게 만들고, 추적 및 디버깅을 어렵게 만들어 암묵적인 결합을 추가합니다.

    한 가지 예시는 클래스에서 훅으로의 전환으로, 여러 생명 주기 이벤트에 걸쳐 분할된 로직이 이제 컴포넌트내에 공존할 수 있는 구성 가능한 함수로 패키지화되는 것입니다.

리액트에 대해 생각하는 좋은 방법은 그 위에 빌드할 수 있는 저수준 기본 요소를 묶음으로 제공하는 라이브러리로 생각하는 것입니다. 원하는 방식으로 유연하게 설계할 수 있다는 점이 장점이자 단점일 수 있습니다.

이는 더 강력한 의견과 추상화를 기반으로 하는 Remix 및 Next와 같은 상위 애플리케이션 수준 프레임워크의 인기와도 관련이 있습니다.

리액트의 확장된 멘탈 모델

리액트가 클라이언트 범위 이상으로 확장되면서 개발자가 풀스택 애플리케이션을 구축할 수 있는 기본 요소를 제공하고 있습니다. 프런트엔드에서 백엔드 코드를 작성하는 것은 새로운 범위의 패턴과 트레이트오프에 대한 문을 열었습니다.

이전의 전환과 비교했을 때, 이번 전환은 이전의 것을 새로 배워야 하는 패러다임의 전환이기보다는 기존에 존재하는 멘탈 모델의 확장에 가깝습니다.

이러한 진화를 더욱 자세히 알아보려면 리액트 모범 사례 다시 생각하기를 확인해보세요. 이 글에서 저는 데이터 로딩과 데이터 뮤테이션을 둘러싼 새로운 패턴의 흐름과 클라이언트 측 캐시에 대해 어떻게 생각하는지에 대해 이야기하고 있습니다.

역주 : 리액트 모범 사례 다시 생각하기 번역글

이는 PHP와 어떻게 다를까요?

PHP와 같이 완전한 서버 주도형 상태 모델에서 클라이언트의 역할은 HTML 수신자 정도입니다. 계산 작업은 서버에 집중되며, 템플릿이 렌더링되고, 라우트 변경 사이의 클라이언트 상태는 전체 페이지 새로고침과 함께 사라집니다.

하이브리드 모델에서 클라이언트와 서버 컴포넌트 모두 전체 컴퓨팅 아키텍처에 기여합니다. 제공하려는 경험의 종류에 따라 규모는 앞뒤로 조절할 수 있습니다.

웹에서 많은 경험을 제공하려는 경우 서버에서 많은 작업을 처리하는 게 합리적입니다. 이를 통해, 클라이언트에서 계산 집약적인 작업을 제거하고 큰 번들을 전송하는 것을 방지할 수 있습니다. 그러나 전체 서버 왕복보다 훨씬 짧은 지연 시간으로 빠른 상호 작용이 필요한 경우 클라이언트 기반의 접근 방식이 더 좋습니다.

리액트는 이 모델의 클라이언트 전용 부분에서 발전했으나, 이제는 리액트가 서버에서 먼저 실행되고 이후 클라이언트 부분에 추가되는 모습을 상상해볼 수 있습니다.

풀스택 리액트 이해하기

클라이언트와 서버를 혼합하려면 모듈 그래프에서 경계가 어디에 있는지 알아야 합니다. 이는 코드가 언제, 어디에서, 어떻게 실행될지 로컬에서 추론하는 데 필요합니다.

이를 위해 리액트에서 그 뒤에 오는 코드의 의미를 변경하는 디렉티브의 형태(또는 프라그마, 리액트 네이티브의 "use strict", 그리고 "use asm", 또는 "worklet"과 유사)로 새로운 패턴이 등장하기 시작했습니다.

  • "use client" 이해하기

    "use client"는 import위의 파일 상단에 배치되어 이어나오는 코드가 "클라이언트 코드"임을 명시하고 서버 전용 코드와의 경계를 표시합니다.

    여기로 import된 다른 모듈(그리고 의존성)은 회선을 통해 전송된 클라이언트 번들의 일부로 간주됩니다.

    클라이언트와 서버라는 용어는 코드가 실행되는 환경을 결정하지 않기 때문에 오직 멘탈 모델의 대략적인 근사치일 뿐입니다.

    "use client"를 사용한 컴포넌트도 서버에서 구동될 수 있습니다. 예를 들어, 초기 HTML 생성의 일부 또는 정적 사이트 생성 프로세스의 일부로 사용할 수 있습니다. 즉, 이것은 오늘날 우리가 알고 있고 사랑하는 리액트 컴포넌트입니다.

  • "use server" 디렉티브

    액션 함수는 클라이언트에서 서버 내의 함수를 호출하는 방법입니다. (원격 프로시저 호출 패턴)

    "use server"는 서버 컴포넌트의 액션 함수 최상단에 위치할 수 있고, 컴파일러에게 서버에 위치하는 코드임을 알려줍니다.

    // 클라이언트로 내려보내지 않고
    // 서버 컴포넌트 내에서
    // 클라이언트가 이 함수를 참조하고 호출할 수 있습니다
    // 서버 (RSC) -> 클라이언트 (RPC) -> 서버 (Action)
    async function update(formData: FormData) {
      'use server'
      await db.post.update({
        content: formData.get('content'),
      })
    }

    Next에서는, "use server"가 파일 최상단에 있을때 번들러에게 모든 export는 서버 액션 함수임을 알립니다. 이를 통해 함수가 클라이언트 번들에 포함되지 않도록 할 수 있습니다.

백엔드와 프런트엔드가 같은 모듈 그래프를 공유할 때, 의도하지 않았던 클라이언트 코드가 실수로 전송될 가능성이 있습니다. 더 나쁜 경우엔 클라이언트 번들로 민감한 데이터를 가져올 수 있습니다.

이를 방지하기 위해 "server-only" 패키지를 통해 경계를 표시하여 뒤에 오는 코드가 오직 서버 컴포넌트에서만 사용되는 것을 보장할 수 있습니다.

이 실험적인 디렉티브와 패턴은 server$와 같은 구분으로 이러한 구분을 표시하는 리액트 외다른 프레임워크에서도 탐구 중입니다.

풀스택 합성

이 전환 과정에서 컴포넌트의 추상화 수준이 더 높아져 서버와 클라이언트 요소를 모두 포함하게 됩니다. 이를 통해 전체 풀스택 버티컬 슬라이스 기능을 재사용하고 합성할 수 있습니다.

// 우리는 서버와 클라이언트 디테일을 모두 캡슐화하는 
// 공유 가능한 풀스택 컴포넌트를 상상해볼 수 있습니다.
<Suspense fallback={<LoadingSkelly />}>
  <AIPoweredRecommendationThing
    apiKey={proccess.env.AI_KEY}
    promptContext={getPromptContext(user)}
  />
</Suspense>

이러한 기능의 대가는 리액트를 기반으로 구축되는 메타 프레임워크에서 발견되는 고급 번들러, 컴파일러, 라우터의 근본적인 복잡성에서 비롯됩니다. 그리고 프런트엔드 개발자로서, 프런트엔드와 동일한 모듈 그래프에서 백엔드 코드를 작성하는 것의 의미를 이해하도록 멘탈 모델을 확장하는 것에서 비롯됩니다.

결론

우리는 믹스인에서 서버 컴포넌트까지 많은 부분을 다루며 리액트의 진화와 각 패러다임에서 트레이드오프의 배경에 대해 탐구해보았습니다.

이러한 변화와 이를 뒷받침하는 원칙을 이해하는 것은 명확한 리액트 멘탈 모델을 정립할 수 있는 좋은 방법입니다. 정확한 멘탈 모델을 갖게 되면 효율적으로 개발하고 버그와 성능 병목 현상을 빠르게 찾아낼 수 있습니다.

큰 프로젝트의 경우, 반쯤 처리된 마이그레이션과 완전하지 않은 아이디어의 패치워크에서 많은 복잡성이 비롯됩니다. 이는 일관된 시각이나 구조가 없을 때 종종 발생합니다. 공유된 이해는 일관성 있게 소통하고 함께 구축하는 데 도움이 되며, 시간이 지남에 따라 적응하고 발전할 수 있는 재사용 가능한 추상화를 형성할 수 있습니다.

앞서 봤듯이, 정확한 멘탈 모델을 구축하는 까다로운 측면은 기존에 존재하는 용어와, 개념을 표현하기 위해 사용하는 언어, 그리고 실제 해당 개념이 어떻게 구현되었는지 사이의 간극입니다.

멘탈 모델을 구축하는 좋은 방법은 특정 접근 방식이나 패턴을 독단적으로 고수하지 않고, 주어진 작업에 적합한 접근 방식을 선택하는 데 필요한 각 방법의 트레이드오프와 이점을 파악하는 것입니다.

참고 자료

3개의 댓글

comment-user-thumbnail
2023년 6월 19일

감사합니다

답글 달기
comment-user-thumbnail
2023년 6월 19일

번역 감사합니다

답글 달기
comment-user-thumbnail
2023년 6월 19일

어우 어렵네요
일단 멘탈 모델이 뭐라는지부터 검색해봣습니다

어려운 만큼 몰랏던 부분이 많고
배워야 할 게 많다고 느껴집니다

좋은 글, 번역 배포해주셔서 감사합니다.

이걸 작성한 사람 REM ?에 대해서도 알 수 있을까요?
어떤 사람이 이정도 통찰력을 지니고 글을 쓸 수 있는지 관심이 가네요 +_+

답글 달기