본문 바로가기
프론트엔드

[React]React와 JSX.

by ISA(류) 2022. 4. 14.

개요

리액트의 경우 기본적으로 jsx라는 자바스크립트 확장 문법을 사용한다. 물론 해당 문법을 사용하지 않아도 사용가능하긴하지만 리액트가 가진 편리함이 많이 줄어든다. 기본적으로 jsx문법을 활용하기 위해서는 babel이나 tsc같은 트랜스파일러를 필요로 하는데 그렇게 치환 할 경우 React.createElements 등의 함수를 호출하는 코드로 트랜스 파일링 된다. 그리고 createElements는 일정한 구조의 객체를 반환한다. 해당 부분에 대한 간단한 흐름을 코드를 통해서 살펴보자.

JSX

본래라면 트랜스파일러 내부를 어느 정도 들여다보아야 정확한 이해가 가능하지만 그럴 경우 관심사가 분산되니 해당 부분은 생략하겠다. 리액트의 바벨 jsx 런타임 관련 코드는 react패키지의 src폴더 내의 jsx폴더 안에 있다.

https://babeljs.io/docs/en/babel-plugin-transform-react-jsx/

 

Babel · The compiler for next generation JavaScript

The compiler for next generation JavaScript

babeljs.io

ReactJSX라는 파일을 확인해보면 22라인의 코드가 보인다.

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */
import {REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';
import {
  jsxWithValidationStatic,
  jsxWithValidationDynamic,
  jsxWithValidation,
} from './ReactJSXElementValidator';
import {jsx as jsxProd} from './ReactJSXElement';
const jsx = __DEV__ ? jsxWithValidationDynamic : jsxProd;
// we may want to special case jsxs internally to take advantage of static children.
// for now we can ship identical prod functions
const jsxs = __DEV__ ? jsxWithValidationStatic : jsxProd;
const jsxDEV = __DEV__ ? jsxWithValidation : undefined;

export {REACT_FRAGMENT_TYPE as Fragment, jsx, jsxs, jsxDEV};

리액트 컴포넌트의 경우 기본적으로 심볼릭 타입을 사용하는데 React fragment의 경우 기타 태그들을 묶어지는 단순한 빈 태그다 보니 shared/ReactSymbols을 살펴보면 바로 심볼릭 타입을 반환한다.

export const REACT_FRAGMENT_TYPE = Symbol.for('react.fragment');

jsx의 경우 validation과 그냥 jsx의 차이가 있는데 둘 차이는 개발 모드에 따른 에러 표시차이 말고는 동일하게 ReactJSXElement의 소스코드를 반환하니 그 부분을 살펴보겠다.

/**
 * https://github.com/reactjs/rfcs/pull/107
 * @param {*} type
 * @param {object} props
 * @param {string} key
 */
export function jsx(type, config, maybeKey) {
  let propName;

  // Reserved names are extracted
  const props = {};

  let key = null;
  let ref = null;

  // Currently, key can be spread in as a prop. This causes a potential
  // issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
  // or <div key="Hi" {...props} /> ). We want to deprecate key spread,
  // but as an intermediary step, we will use jsxDEV for everything except
  // <div {...props} key="Hi" />, because we aren't currently able to tell if
  // key is explicitly declared to be undefined or not.
  if (maybeKey !== undefined) {
    if (__DEV__) {
      checkKeyStringCoercion(maybeKey);
    }
    key = '' + maybeKey;
  }

  if (hasValidKey(config)) {
    if (__DEV__) {
      checkKeyStringCoercion(config.key);
    }
    key = '' + config.key;
  }

  if (hasValidRef(config)) {
    ref = config.ref;
  }

  // Remaining properties are added to a new props object
  for (propName in config) {
    if (
      hasOwnProperty.call(config, propName) &&
      !RESERVED_PROPS.hasOwnProperty(propName)
    ) {
      props[propName] = config[propName];
    }
  }

  // Resolve default props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  return ReactElement(
    type,
    key,
    ref,
    undefined,
    undefined,
    ReactCurrentOwner.current,
    props,
  );
}

jsx와 jsxDEV라는 함수를 호출하여서 리액트 Element 객체를 만들어주는데 두 함수 간의 차이는 개발 모드인지에 따라서 조금 달라지는 것을 제외하고는 동일하기에 jsx 함수를 기준으로 보겠다.

입력 받는 인자로 type(태그이름), config(props속성), key가 존재한다. DEV의 경우 추가로 self와 source를 받는데 둘다 데브툴에서 디버깅에 사용되는 속성이므로 무시해도 상관 없다.

기본적으로 key와 ref를 체크하고 바로 입력 받은 type과 config를 통해서 Element의 props 객체를 만들어준다. 보통 태그의 속성과 해당 태그의 자식등이 포함된다.(className이나 children)

// Remaining properties are added to a new props object
  for (propName in config) {
    if (
      hasOwnProperty.call(config, propName) &&
      !RESERVED_PROPS.hasOwnProperty(propName)
    ) {
      props[propName] = config[propName];
    }
  }

  // Resolve default props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

그 후 그렇게 만들어진 데이터들을 ReactElement라는 함수에 인자로 호출해서 반환하는데 해당 코드의 경우 아래와 같다.

/**
 * Factory method to create a new React element. This no longer adheres to
 * the class pattern, so do not use new to call it. Also, instanceof check
 * will not work. Instead test $$typeof field against Symbol.for('react.element') to check
 * if something is a React Element.
 *
 * @param {*} type
 * @param {*} props
 * @param {*} key
 * @param {string|object} ref
 * @param {*} owner
 * @param {*} self A *temporary* helper to detect places where `this` is
 * different from the `owner` when React.createElement is called, so that we
 * can warn. We want to get rid of owner and replace string `ref`s with arrow
 * functions, and as long as `this` and owner are the same, there will be no
 * change in behavior.
 * @param {*} source An annotation object (added by a transpiler or otherwise)
 * indicating filename, line number, and/or other information.
 * @internal
 */
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  if (__DEV__) {
    // The validation flag is currently mutative. We put it on
    // an external backing store so that we can freeze the whole object.
    // This can be replaced with a WeakMap once they are implemented in
    // commonly used development environments.
    element._store = {};

    // To make comparing ReactElements easier for testing purposes, we make
    // the validation flag non-enumerable (where possible, which should
    // include every environment we run tests in), so the test framework
    // ignores it.
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    // self and source are DEV only properties.
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    // Two elements created in two different places should be considered
    // equal for testing purposes and therefore we hide it from enumeration.
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};

$$typeof:는 심볼로 구성된 리액트 컴포넌트 종류에 해당하고

type은 해당 HTML태그의 종류 key는 react key props(보통 쓰지 않지만 동적으로 컴포넌트를 생성할시 리액트 컴포넌트 측에 해당 컴포넌트를 식별할 키로 주는 값이다.)와 dom에 접근하기 위한 ref 그리고 역시 props와 owner로 이루어져있다. 해당 코드 자체는 그렇게 어렵지도 않고, 사실 그저 일정한 구조의 객체를 만드는 것이다 보니 따로 설명 할 필요는 없는 것 같다.

원래라면 babel을 한번 뜯어서 react의 jsx를 정확히 어떻게 파싱해서 사용하는지 봐야하지만 바벨 jsx의 경우 해당 런타임들을 사용하거나 트랜스파일링이 끝난 코드의 경우 react.createElement 함수를 호출하는 코드로 변경 되는데 해당 코드들의 경우 src폴더의 ReactElement와 validator라는 jsx 폴더 안의 구조와 비슷한 구조로 된 파일에 중복해서 존재한다.

/**
 * Create and return a new ReactElement of the given type.
 * See https://reactjs.org/docs/react-api.html#createelement
 */
export function createElement(type, config, children) {
  let propName;

  // Reserved names are extracted
  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;

      if (__DEV__) {
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    if (hasValidKey(config)) {
      if (__DEV__) {
        checkKeyStringCoercion(config.key);
      }
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }

  // Resolve default props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  if (__DEV__) {
    if (key || ref) {
      const displayName =
        typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

서로간의 코드에 별다른 차이점이 없고 실제로 중복되는 코드가 같이 존재하며 create와 clone에 해당하는 이름을 가진 함수가 추가로 있어 해당 부분을 내보내기 하고 있다. 물론 몇가지 config가 다르긴하지만 (유의미하다고 볼 수 없을 정도다.)관련해서 이슈를 찾아본 결과 중복 코드들은 사실상 바벨 플러그인에 대한 호환성 지원 차원에서 중복으로 유지하는 것으로 보이므로 그렇게 신경 쓸 필요는 없는 것 같지만 신경이 쓰인다면 아래의 링크를 통해서 이슈를 확인 해보길 바란다.

https://github.com/facebook/react/pull/18299​

결론

이렇게 리액트에서 jsx를 어떻게 처리하는지 또 React element가 어떤 구조로 되어 있고 어디까지가 렌더러가 아닌 React의 역할인지 간단히 살펴보았다. React 패키지의 경우 사실 이 이상 기능을 가지지 않는데 기본적인 타입 설정이나 리액트 객체 또 jsx 정도 개발자 모드에서 출력하는 각종 경고 정도 요소를 가지고 있으며 그 외 기능은 react-dom이나 리액트 네이티브 같은 렌더러가 담당한다.

반응형

'프론트엔드' 카테고리의 다른 글

프론트엔드 설계 고민 -4-  (0) 2022.05.21
프론트엔드 설계 고민 -3-  (0) 2022.04.26
리액트 포탈과 렌더링 고민  (0) 2022.04.09
프론트엔드 설계 고민 -2-  (0) 2022.04.08
프론트엔드 설계 고민 -1-  (0) 2022.04.05