ReactJS 를 날로 먹어보자!

Table of Contents

먼저..

https://gitlab.ntiple.com/developers/reactjs-onthefly-sample

나는 리액트JS 를 싫어한다!

늘 비교되는 vuejs 에 비해 직관적이지도 않고 불편하고,
협업할때 디자인 요소가 분리되어 있지 않아서
매번 퍼블리셔들과 싸워야 하고..

무엇보다.

코드가 너무 못생겼다.!

아무리 예쁘게 코딩하려고 노오력 해 봐도 똑같은 기능을 하는 vuejs결과물 에 비해 reactjs로 작성된 코드를 보고 있자면.. 눈과 맘이 아파온다.

(참고로 나는 못생기게 짠 코드를 못견디는 편이다... 맞춤법을 매우 틀리게 쓰는 상대랑 개인톡 하는느낌.... 이랄까..)

그래도.. 뭐...

먹고 살자니 적응 해야지..

그치만 vuejs 도 그렇고 모던 프론트엔드 프로젝트 들은 준비해야 할게 너무 많다.

nodejs 설치하고 vscode 설치하고 프레임워크 설정하고 의존 라이브러리 설치하고....

하다보면 그것만으로도 반나절은 간다..

요컨데 맛만 살짝 좀 보려 했는데. 본격 주방장 데려오는 판.... 이랄까.

그래서!!!!!

맛만 좀 보는김에..

리액트JS를 아예 날것으로 먹어보자!

준비할건 브라우저와 자신에게 익숙한 편집기 뿐!

인터넷을 뒤져가며 몇번의 시행착오를 거쳐 브라우저만으로 작동하는 기초코드를 아래와 같이 만들어 보았다.
(프레임워크 대부분은 cdnjs 에서 import 하였다 / 연식이 오래된 브라우저에서는 작동 안될수도......)

<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- [캐시방지 -->
    <meta httpEquiv='cache-control' content='max-age=0' />
    <meta httpEquiv='cache-control' content='no-cache' />
    <meta httpEquiv='expires' content='0' />
    <meta httpEquiv='expires' content='Tue, 01 Jan 1980 1:00:00 GMT' />
    <meta httpEquiv='pragma' content='no-cache' />
    <!-- ]캐시방지 -->
    <meta charset='utf-8' />
  </head>
  <body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.24.7/babel.min.js" integrity="sha512-bAHF//mCdqGSgyUBqhtDgaGLxsraipURsQRGG+3uNncZdsFA6/283u21SOwB6rzINUXSATUMoZaXm4IaV2Lw2Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script data-plugins="transform-modules-umd" type="text/babel" data-presets="react" data-type="module">
  const { render } = ReactDOM
  const onClick = () => {
    alert('OK')
  }
  render(
  <>
    <div>
      리액트JS를 날로 먹어보자
    </div>
    <button
      onClick={ onClick }
      >
      버튼
    </button>
  </>
  , document.body)
  </script>
  </body>
</html>

좀더 간단하게 시작할수 있었는데 굳이 babel 을 사용한 것은 브라우저 내에서 jsx 문법을 사용하기 위함이다

참고로 아쉽게도 자동완성 등은 지원하지 않으며, 거의 모든 컴포넌트가 window 객체 에 등록될 예정이기 때문에 보안상으로도 매우 취약하다.

뭐.. 이걸 실전에 쓰려는 사람은 없을테니...

말그대로 맛만 보자.!

이제 재료는 준비되었으니.

본격적으로 기능들을 구현해 보자. 요구사항은 대충....

  • 일단은 필요한 최소한만 구현하고 싶다.

  • 파일단위로 컴포넌트를 작성하고 싶다.

  • vuejs 처럼 컴포넌트 작동시 그때그때 데이터에 반영하고 싶다.

라는 취지로. 코딩해 보자.

  • /app.js 는 전형적인 ReactJS 코드로 아래와 같이 코딩했다.

const { useState, useEffect } = React

export default () => {
  const [state, setState] = useState(0)
  const [data] = useState({
    formdata: {
      input: ''
    }
  })
  useEffect(() => {
    console.log('APP START...')
    return () => {
      console.log('APP DESTROY...')
    }
  }, [state, data])
  return (
    <>
    <main>
      <hr/>
      <section>
        <h3>테스트</h3>
        <div>
          <input
            onChange={ e => {
              data.formdata.input = e.target.value
              setState(state + 1)
            } }
            />
        </div>
      </section>
      <hr/>
      <section>
        <h3>FORMDATA</h3>
        <div>
          { JSON.stringify(data.formdata) }
        </div>
      </section>
    </main>
    </>
  )
}

  • /index.html 을 아래와 같이 추가 / 수정해 준다

... 중략 ...

  <script data-plugins="transform-modules-umd" type="text/babel" data-presets="react" data-type="module" src="./app.js"></script>
  <script data-plugins="transform-modules-umd" type="text/babel" data-presets="react" data-type="module">
  const App = app.default
  ReactDOM. render(<App/>, document.body)
  </script>
  </body>
</html>

브라우저로 index.html 을 실행했을 때 아래와 같이 나오면 성공!

그래. 이제 맛은 쬐끔 봤으니

이제부터 여기다가 양념을 조금 쳐 볼 셈이다.

It tastes like so shit !!!!!!

우선... 앞서 말했듯 나는 리액트를 아주 싫어한다.
그래서 리액트를 리액트같지 않게 사용할수 있도록 살짝만 개조할건데.
리액트 저렇게 쓰는거 아닌데!! 라고 불편해 할 사람이 있을지도 모르겠다. ㅋ

  • 스크립트 로더 (/libs/loader.js)

/** 동적로딩을 위해 필요한 스크립트 */
const loader = {
  load: (src, opt = {}) => {
    let imp = ''
    if (window && window.document && window.document.readyState == 'complete') {
      /** FIXME: 아직 document 완성 후 lazy-loading 은 지원하지 않는다. */
      console.log('cannot import by lazy-loading')
    } else {
      /** cache 유효 시간은 5초단위 */
      let timeid = Number(new Date().getTime())
      timeid = timeid - (timeid % 5000)
      if (opt && opt.module) {
        /** 기본적으로는 cache 하지 않도록 */
        if (!opt.cache) {
          if (/[?]/.test(src)) {
            src = `${src}&${timeid}`
          } else {
            src = `${src}?${timeid}`
          }
        }
        imp = `<script data-plugins="transform-modules-umd" type="text/babel" data-presets="react" data-type="module" src="${src}"></script>`
      } else {
        imp = `<script src="${src}"></script>`
      }
      if (imp) {
        document.write(imp)
      }
    }
  }
}

  • 공통 라이브러리 (/libs/commons.js)

const lodash = _
const { debounce } = lodash
const { render } = ReactDOM
const { useState, useEffect, useRef, forwardRef } = React
const log = { trace: () => { }, debug: console.log, warn: console.log, error: console.log }

/** 상수정의 */
const $$forceupdate$$ = '$$forceupdate$$'

/**
 * 컴포넌트 업데이트 메소드, vue 의 forceUpdate 와 비슷하다
 * 인자로 받아들이는 state 값이 기존 저장된 값과 달라야 update 이벤트 발생
 * 따라서 전역 state 와 동기화 시키면 이걸 사용하는 모든 컴포넌트가 반응한다
 **/
const useUpdate = (delay = 10) => {
  const [_, fncupdate] = useState(0)
  return debounce((v) => { fncupdate(v) }, delay)
}

/**
 * vuejs 의 mounted / unmounted 개념처럼 컴포넌트의 생성, 종료 시점에 수행될 스크립트를 지정
 * 다음과 같이 사용 가능하다
 * useLauncher({
 *   async start() {
 *     console.log('시작')
 *   },
 *   async destroy() {
 *     console.log('종료')
 *   },
 * })
 */
const useLauncher = (prm) => {
  const [phase, setPhase] = useState(0)
  useEffect(() => {
    const pprm = prm
    let retproc = () => { }
    let res =  undefined
    switch (phase) {
    case 0: {
      setPhase(1)
      if (pprm.start) {
        setTimeout(async () => {
          if (window.appContext) {
            /** 시작스크립트 */
            res = await pprm.start()
            setPhase(2)
          }
        }, 0)
      } else {
        setPhase(2)
      }
    } break
    case 1: { } break
    case 2: {
      if (pprm.work) {
        res = pprm.work()
        if (res instanceof Promise) {
          res.then(res => {
            if (res) { setPhase(3) }
          })
        } else if (res) {
          setPhase(3)
        }
      } else {
        setPhase(3)
      }
    } break
    case 3: {
      /** 종료스크립트 */
      if (pprm.destroy) { return prm.destroy }
    }}
    return retproc
  }, [phase])
  return phase
}

/** 난수생성 */
const getRandom = (max, min = undefined) => {
  if (min == undefined) { min = 0 }
  if (max < 0) { max = max * -1 }
  const ret = min + Math.floor(Math.random() * max)
  return ret
}

/** 난수 문자 생성 */
const randomChar = (c = 'a', n = 26) => {
  return String.fromCharCode(Number(c.charCodeAt(0)) + getRandom(n))
}

/** 난수문자열 생성 */
const randomStr = (length, type = undefined) => {
  let ret = ''
  switch (type) {
  case undefined:
  case 'number':
    for (let inx = 0; inx < length; inx++) {
      ret += String(getRandom(10))
    }
    break
  case 'alpha':
    for (let inx = 0; inx < length; inx++) {
      switch(getRandom(2)) {
      case 0:
        /** 소문자 */
        ret += randomChar('a', 26)
        break
      case 1:
        /** 대문자 */
        ret += randomChar('A', 26)
        break
      }
    }
    break
  case 'alphanum':
    for (let inx = 0; inx < length; inx++) {
      switch(getRandom(3)) {
      case 0:
        /** 숫자 */
        ret += String(getRandom(10))
        break
      case 1:
        /** 소문자 */
        ret += randomChar('a', 26) 
        break
      case 2:
        /** 대문자 */
        ret += randomChar('A', 26)
        break
      }
    }
    break
  }
  return ret
}

/** 지정된 객체에 모든 원소를 붙여넣기 한다. */
const putAll = (target, source) => {
  if (target == null) { return target }
  if (source == null) { return target }
  for (const k in source) {
    const titem = target[k]
    const sitem = source[k]
    if (titem !== undefined) {
      if (typeof (titem) === 'string') {
        target[k] = source[k]
      } else if (titem instanceof Array && sitem instanceof Array) {
        putAll(titem, sitem)
      } else if (typeof(titem) === 'object' && typeof(sitem) === 'object') {
        putAll(titem, sitem)
      } else {
        /** 타입이 다르다면 무조건 치환. */
        target[k] = source[k]
      }
    } else {
      target[k] = source[k]
    }
  }
  return target
}

/** 배열에 값채워넣기 */
const arrayFill = (a, v = undefined) => {
  let ret = a
  if (!a) { return ret }
  if (!(a instanceof Array)) { return ret }
  a.map((_, inx) => a[inx] = v )
  return ret
}

/** react props 사본을 생성한다 (특정 키값은 제외) */
const propsExclude = (props, excludes = []) => {
  let ret = { }
  const keys = Object.keys(props)
  for (const key of keys) {
    if (excludes.indexOf(key) !== -1) { continue }
    if (['ref', 'key'].indexOf(key) !== -1) { continue }
    ret[key] = (props)[key]
  }
  return ret
}

/** 다기능 appContext 생성, 모든 컴포넌트 전역에 영향을 끼침 */
const createAppContext = () => {
  /** redux 객체 생성 */
  const slice = createSlice({
    name: 'appContext',
    initialState: { gstate: 0 },
    reducers: {
      incrementGState: (state, action) => {
        if ((action.payload || 0) > state.gstate) {
          state.gstate = action.payload
        }
      }
    }
  })
  const store = configureStore({
    reducer: slice.reducer
  })
  /** appContext 생성 */
  const appContext = {
    /** 전역상태변수 접근 (모든 컴포넌트에게 업데이트 통보수단) */
    gstate: (add = 0) => {
      if ((add !== 0 && !add) || isNaN(add)) { add = 0 }
      let gstate = store.getState().gstate + add
      if (add !== 0) { appContext._dispatchGstate(gstate) }
      return gstate
    },
    /** 너무 자주 수행되지 않도록 debounce 를 걸어준다 */
    _dispatchGstate: debounce((state = 0) => {
      store.dispatch(slice.actions.incrementGState(state)) 
    }, 10),
    /** 전역상태변수 상태를 모니터링 하도록 구독한다. */
    subscribe(fnc, delay = 0) {
      const debounced = debounce(fnc, delay)
      store.subscribe(debounced)
    },
    /** 데이터모델 변화시 DOM 에 강제 업데이트 할때 사용 */
    isPutModel(model) {
      return model && model[$$forceupdate$$]
    },
    putModel: (model, mode = true) => {
      if (mode) {
        model[$$forceupdate$$] = true
        appContext.gstate(1)
      } else {
        if (model[$$forceupdate$$]) {
          appContext._removeUpdate(model)
          setTimeout(() => appContext.gstate(1), 100)
        }
      }
    },
    _removeUpdate: debounce((model) => {
      delete model[$$forceupdate$$]
    }, 10)
  }
  window.appContext = appContext
}

  • 버튼 컴포넌트 (/components/button.js),

/**
 * 버튼컴포넌트, 일반 `button` 과 같다.
 * 나중에 공통작업 할것을 대비해 atom-component 로 작성.
 */
export default forwardRef((props, ref) => {
  const pprops = propsExclude(props, [''])
  const elem = useRef()
  const onClick = async () => {
    if (props?.onClick) {
      return props?.onClick()
    }
  }
  return (
    <>
    <button
      ref={ elem }
      onClick={ onClick }
      { ...pprops }
      >
      { props.children }
    </button>
    </>
  )
})

  • 입력 컴포넌트 (/components/input.js),

/**
 * 입력 컴포넌트, 입력과 동시에 데이터모델에 값을 전달하도록 설계
 */

/** unserializable 객체들을 다루기 위해 별도 공용저장소 사용 */
const ctx = { }
export default forwardRef((props, ref) => {
  /** model 속성을 제외한 props 복제 생성 */
  const pprops = propsExclude(props, ['model'])
  /** 공용저장소 식별자 */
  const [id] = useState(`${new Date().getTime()}${randomStr(3, 'number')}`)
  /** 데이터모델 */
  const pmodel = props?.model
  const pname = props?.name ? props.name.split(/[.]/)[0] : undefined
  /** input html 객체 */
  const elem = useRef()
  const onChange = async (e) => {
    const v = elem.current.value
    /** 변경시 데이터모델에 값전달 */
    if (pmodel && pname) {
      pmodel[pname] = v
      update(appContext.gstate(1))
    }
    /** 그 외 바인드된 핸들러가 있다면 수행 */
    if (props?.onChange) {
      return props.onChange(e)
    }
  }
  const update = useUpdate()
  ctx[id] = { props: props, elem: elem }
  useLauncher({
    async start() {
      if (ref && ref.hasOwnProperty('current')) { ref.current = elem.current }
      if (pmodel && pname) { elem.current.value = pmodel[pname] }
      /** 컴포넌트 외부에서 전역 업데이트 요청이 들어온경우 */
      appContext.subscribe(async () => {
        /** 데이터모델에 강제업데이트 요청이 있다면 DOM 수정 */
        if (pmodel && pname && appContext.isPutModel(pmodel)) {
          elem.current.value = pmodel[pname]
          appContext.putModel(pmodel, false)
        }
        /** 현재 컴포넌트 상태 업데이트 (전역상태와 동기화) */
        update(appContext.gstate())
      })
    },
    async destroy() {
      /** 공용저장소에 저장된 내용 삭제 */
      delete ctx[id]
    }
  })
  return (
    <>
    <input
      ref={ elem }
      onChange={ onChange }
      /** 기타 모든 속성은 상위객체에서 가공없이 받아넘긴다 */
      { ...pprops }
      />
    </>
  )
})

  • 체크박스 컴포넌트 (/components/checkbox.js),

/**
 * 체크박스 컴포넌트, 입력과 동시에 데이터모델에 값을 전달하도록 설계
 */

/** unserializable 객체들을 다루기 위해 별도 공용저장소 사용 */
const ctx = { }
export default forwardRef((props, ref) => {
  /** model 속성을 제외한 props 복제 생성 */
  const pprops = propsExclude(props, ['model'])
  /** 공용저장소 식별자 */
  const [id] = useState(`${new Date().getTime()}${randomStr(3, 'number')}`)
  /** 데이터모델 */
  const pmodel = props?.model
  const pname = props?.name ? props.name.split(/[.]/)[0] : undefined
  const pinx = props?.name ? props.name.split(/[.]/)[1] : 0
  /** input html 객체 */
  const elem = useRef()
  /** 외부에서 변경된 값을 DOM 에 적용 (체크 상태 변경) */
  const updatechk = () => {
    let ret = (pmodel && pname) ? pmodel[pname] : undefined
    if (typeof ret == 'object') { ret = ret[pinx] }
    if (props.value !== undefined) {
      elem.current.checked = (props.value == ret)
    } else {
      elem.current.checked = (!!ret)
    }
    return ret
  }
  const onChange = async (e) => {
    const v = elem.current.checked
    /** 변경시 데이터모델에 값전달 */
    if (props.value !== undefined) {
      if (pmodel && pname) {
        if (typeof pmodel[pname] == 'object') {
          pmodel[pname][pinx] = v ? props.value : ''
        } else {
          pmodel[pname] = v ? props.value : ''
        }
      }
    }
    update(appContext.gstate(1))
    if (props?.onChange) {
      return props.onChange(e)
    }
  }
  const update = useUpdate()
  ctx[id] = { props: props, elem: elem }
  useLauncher({
    async start() {
      if (ref && ref.hasOwnProperty('current')) { ref.current = elem.current }
      updatechk()
      /** 컴포넌트 외부에서 전역 업데이트 요청이 들어온경우 */
      appContext.subscribe(async () => {
        updatechk()
        /** 현재 컴포넌트 상태 업데이트 (전역상태와 동기화) */
        update(appContext.gstate())
      })
    },
    async destroy() {
      /** 공용저장소에 저장된 내용 삭제 */
      delete ctx[id]
    }
  })
  return (
    <>
    <input
      type='checkbox'
      ref={ elem }
      onChange={ onChange }
      /** 기타 모든 속성은 상위객체에서 가공없이 받아넘긴다 */
      { ...pprops }
      />
    </>
  )
})

  • React 페이지 (/app.js)

const Input = input.default
const Checkbox = checkbox.default
const Button = button.default

export default () => {
  const [data] = useState({
    formadata: {
      input: 'test000',
      checkbox1: '',
      checkbox2: '',
      checklist: ['', '', '', '']
    }
  })
  const doClear = async () => {
    data.formadata.input = ''
    data.formadata.checkbox1 = ''
    data.formadata.checkbox2 = ''
    arrayFill(data.formadata.checklist, '')
    /** formdata 와 바인드 된 모든 컴포넌트에게 업데이트 전파 */
    appContext.putModel(data.formadata)
  }
  const update = useUpdate()
  useLauncher({
    async start() {
      appContext.subscribe(async () => {
        /** 전역 state 동기화 업데이트 */
        update(appContext.gstate())
      })
    },
    async destroy() {

    }
  })
  return (
    <>
    <main>
      <hr/>
      <section>
        <h3>INPUT</h3>
        <div>
          <Input
            model={ data.formadata }
            name='input'
            />
        </div>
      </section>
      <hr/>
      <section>
        <h3>CHECKBOX</h3>
        <div>
          {/* 체크박스 */}
          <Checkbox
            model={ data.formadata }
            name='checkbox1'
            value='y'
            />
          {/* 여기서부터는 radio 같이 택일 방식으로 작동한다 */}
          <Checkbox
            model={ data.formadata }
            name='checkbox2'
            value='a'
            />
          <Checkbox
            model={ data.formadata }
            name='checkbox2'
            value='b'
            />
          <Checkbox
            model={ data.formadata }
            name='checkbox2'
            value='c'
            />
        </div>
      </section>
      <hr/>
      <section>
        <h3>CHECKLIST</h3>
        <div>
          {/* 다수개의 체크박스를 관리해야 할경우 */}
          { data.formadata.checklist.map((itm, inx) => (
            <Checkbox
              model={ data.formadata }
              name={ `checklist.${inx}` }
              value='t'
              />
          )) }
        </div>
      </section>
      <hr/>
      <section>
        <h3>BUTTON</h3>
        <div>
          <Button
            onClick={ doClear }
            >
            CLEAR
          </Button>
        </div>
      </section>
      <hr/>
      <section>
        {/* 모델데이터를 덤프하여 보여준다 */}
        <h3>FORMDATA</h3>
        <div>
          { JSON.stringify(data.formadata) }
        </div>
      </section>
      <hr/>
      <h3>RANDOM</h3>
      <section>
        <div> { `${new Date().getTime()}${randomStr(3, 'number')}` } </div>
        <div> { `${new Date().getTime()}${randomStr(3, 'number')}` } </div>
        <div> { `${new Date().getTime()}${randomStr(3, 'number')}` } </div>
        <div> { `${new Date().getTime()}${randomStr(3, 'number')}` } </div>
        <div> { `${new Date().getTime()}${randomStr(3, 'number')}` } </div>
      </section>
    </main>
    </>
  )
}

  • 실행페이지 (/index.html),

<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta charset='utf-8' />
    <!-- [캐시방지 -->
    <meta httpEquiv='cache-control' content='no-cache, no-store, must-revalidate, max-age=0' />
    <meta httpEquiv='expires' content='0' />
    <meta httpEquiv='expires' content='Tue, 01 Jan 1980 1:00:00 GMT' />
    <meta httpEquiv='pragma' content='no-cache' />
    <!-- ]캐시방지 -->
    <script src="./libs/loader.js"></script>
  </head>
  <body>
  <!-- [프레임워크 / 외부 인터넷 없이 사용하는경우 -->
  <!-- <script src="./assets/scripts/babel-7.24.7.min.js"></script>
  <script src="./assets/scripts/lodash-4.17.21.min.js"></script>
  <script src="./assets/scripts/jquery-3.7.1.min.js"></script>
  <script src="./assets/scripts/react-18.3.1.min.js"></script>
  <script src="./assets/scripts/react-dom-18.3.1.min.js"></script>
  <script type="module">
  import { createSlice, configureStore } from './assets/scripts/reduxjs-toolkit-2.2.5.esm.min.js'
  putAll(window, { createSlice, configureStore })
  </script> -->
  <!-- ]프레임워크 -->
  <!-- [프레임워크(CDN) / 인터넷이 가능한경우 사용 -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.24.7/babel.min.js" integrity="sha512-bAHF//mCdqGSgyUBqhtDgaGLxsraipURsQRGG+3uNncZdsFA6/283u21SOwB6rzINUXSATUMoZaXm4IaV2Lw2Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script type="module">
  /** redux 를 사용하기 위해 사용할 함수를 window 객체로 꺼낸다 */
  import { createSlice, configureStore } from 'https://cdn.jsdelivr.net/npm/@reduxjs/toolkit@2.2.5/+esm' 
  putAll(window, { createSlice, configureStore })
  </script>
  <!-- ]프레임워크(CDN) -->
  <!-- 작성한 컴포넌트는 여기서 아래와 같이 모두 명시적으로 import 해 주어야 작동한다 -->
  <script>
  loader.load('./libs/commons.js')
  loader.load('./components/input.js', { module: true })
  loader.load('./components/checkbox.js', { module: true })
  loader.load('./components/button.js', { module: true })
  loader.load('./app.js', { module: true })
  </script>
  <script data-plugins="transform-modules-umd" type="text/babel" data-presets="react" data-type="module">
  createAppContext()
  const App = app.default
  render(<App/>, document.body)
  </script>
  </body>
</html>

  • 실행결과

마무리

일단은 ReactJS 를 날것으로 먹어봤다. (배탈이 날거 같다..)

썩 좋아보이지는 않지만 그래도 돌려보는데 의의를 둔다면 뭐...

실전에 쓰려면 무조건 환경을 갖추고 실행하는걸 권장한다.

기회가 된다면 같은 기능을 하는 VUE 코드와 React 코드를 같이두고 리뷰해 보는것도 재미있을듯

뭐. 여튼 오늘 삽질은 여기까지.!

본 포스트의 결과물 실행결과 및 소스 전체 내용은 아래 주소에서 확인 / 다운받아 볼 수 있다

https://gitlab.ntiple.com/developers/reactjs-onthefly-sample
https://devlog.ntiple.com/samples/sample-808/src
https://devlog.ntiple.com/samples/sample-808-1/src

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다