[카테고리:] 미분류 (Page 1 of 2)

드리밍 인 코드

검색 중 2009년 작성한 도서 리뷰(드리밍 인 코드)를 발견했다. 십년을 넘게 유지해오던 블로그를 호스팅 업체에서 날려먹으면서 허탈해진 기억이 난다. 그나마 여기저기 인터넷에 남아 있는 내 기록의 파편들을 발견해서 약간은 위안이 된다.

이제 그 파편이라도 여기 다시 뿌려 본다.

드리밍 인 코드(Dreaming in code) – 막장이 되는 이유

2009.03.31.

지난 달에 구입해서 정말 재미있게 본 책입니다. 저자인 스콧 로젠버그는 컬럼니스트이지만 아마추어 개발자로 챈들러 프로젝트에 몸을 담고 그 프로젝트의 3년간을 기술합니다. 챈들러 프로젝트는 미치 케이퍼, 알랜 케이등 전설급의 프로그래머들이 OSAF로 구성되어 진행되었지만 책에 기록된 3년은 그야말로 막장으로 보입니다. 챈들러는 현재 1.0.2가 릴리즈되어 있고 http://chandlerproject.org/ 에서 확인할 수 있습니다.

전설급 프로그래머들이 모여있고, 무한에 가까운 자금이 있으면서, 딱 정해진 일정도 없이(완전이 일정이 없는 것은 아니지만 SI 프로젝트와 같이 살을 죄는 듯한 일정은 아니니까..) 자유스러운 분위기의 프로젝트가 왜 막장이 되어가는지도 섬세히 기록되어 있습니다.

놀라운 것은 필자가 프로 개발자가 아닌데도 풍부한 지식으로 이야기를 풀어나가고 개발방법론, 프로그램 역사 등을 세세히 알려주는데, 그 깊이에 감탄했습니다.

흔히들 SI 프로젝트 하면 막장이라고 표현되는데요, 챈들러가 막장이 되어 가는 과정이나 이유가 막장 SI 프로젝트와 유사한 점들이 발견됩니다. SI 프로젝트 하면 갑을병정의 발주처-원청-하청-하청의 하청 구조와, 말도 안되는 일정, 끊임없이 실시간으로 바뀌어나가는 요구사항, 지쳐 떨어지는 개발자와 다시 충원되는 개발자, 연결고리가 약한 여러 하청의 조직 등등 많은 이유가 있습니다.

챈들러와 SI 프로젝트와의 공통점은 잦은 변경입니다. 설계부터 흔들리는 요구사항 변경은 프로젝트를 막장으로 만드는데 1등공신이죠. 물론 SI와 챈들러 사이의 요구사항 변경 이유야 절실히 다르지만요. 챈들러는 미치 케이퍼의 아젠다라는 비전이 있지만 SI야 고객 기분에 따라, 고객 직책에 따라 요구사항이 바뀌는 거니까요. 그렇지만 요구사항이 바뀌는 근본적인 이유가 무엇인지 이 책을 보면서 스스로 깨닭았습니다. 그건 “개발해야할 대상이 무엇인지 모른다.”였습니다.

챈들러는 아젠다의 비전에 따라 이전까지 없던 프로그램이었기 때문에 고민하고 연구했겠지만 SI프로젝트는 두리뭉실한 목적하에 고객도 모르고 개발자도 모르는 정체불명의 시스템을 만들어야 한다는 겁니다. 하긴 대개의 프로젝트는 이미 있던 업무(손으로 하든 엑셀로 하든)를 전산 시스템으로 구현하는 거라 완전히 모른다고는 할 수 없지요. 또 비슷비슷한 프로젝트를 진행한 개발자도 있을 것이며 경험많은 PM등 제대로 분석할 수 있는 경우도 있습니다. 그러면 뭐합니까.. 고객이 모르는데요. 이 빌어먹을 고객이라는 인종은 현재 자신들이 하고 있는 업무 조차 헷갈려한다는게 문제지요.

예를 들면 요전에 제가 재고수불관리시스템 프로젝트를 진행했는데요, 재고수불이라는 게 생산한 제품들의 모든 이력은 그때그때 남기고 그 총 합이 현재고와 딱 맞는다는 간단한 원리로부터 시작됩니다. 시스템 구현이 어느정도 끝나가고 있을 시점, 그러니까 테스트 버전을 고객이 볼 수 있는 시점부터 진정한 막장이 시작되었습니다. 사용자는 데이터를 삭제했다 살렸다 하는 업무를 반복할 수 있는데 그 데이터는 무의미하다고 기록하지 말라는 겁니다. 모든 이력을 남기는 것이 저 수불이라는 것인데 기록하지 말라니요. 물론 처음에는 고객이 동의했고 이해했다고 생각했는데, 제가 잠시 착각한 거였습니다. 고객은 아무것도 모르고 있었습니다. 아아.. 무식이 죄지만.. 그 무식이 사람 여럿 잡았지요. 문제는 그 잘못된 생각(비단 저 하나의 예시 뿐 아니라 꽤나 많은 끔찍한 요구들을 포함하여)이 설득도 되지 않고 절대 꺾이지도 않는다는 점입니다. 결국 시스템의 품질은 개판이 되는 거죠. 더욱이 더 큰 문제는 고객이라는 인종이 전문가인 해당 프로젝트의 개발자들(PM을 포함하여)의 말을 인정하지 않는다는 것입니다. 그들의 그때그때 바뀌는 생각이 항상 옳으며 그대로 따라야 제대로 된다는 것이죠. 프로젝트가 어느 정도 막바지에 이르게 되면 프로젝트를 진행했던 각 팀원들이 열심히 본업에 충실했다면 현업만큼 또는 사람에 따라서 현업보다 훨씬 업무를 잘 이해하고 진행할 수 있지만, 고객이라는 인종은 이 정도는 고사하고 개발자를 전문가로 인정하는 생각이 뇌속에서 제거된 상태입니다. 

챈들러 프로젝트를 진행했던 구성원들은 이상을 고수하며 현실을 위해 투쟁하는 모습이 책에 잘 그려져 있습니다. 그들은 그들 나름대로 깨우친 것들이 있었겠지요. 로젠버그는 그것을 “소프트웨어를 개발하는 것이 가장 어렵다.”라고 말한 TAOCP의 도널드 커누스 교수의 말을 인용하여 표현해 놓았습니다. 그리고 결정적으로 사람들은(프로그래머를 포함하여) 소프트웨어가 무엇인지 모른다고 말합니다. 그래서 해야 할 것들이 많다는 것이죠.

책을 덮고 나서, 잘 알기 어려운 이 소프트웨어라는 것을 제대로 현실로 이끌어낼 사람은 프로그래머밖에 없다는 생각이 들었습니다. 제겐 너무나 다행이고, 저를 흥분시키게 하네요. 비록 지금은 썩 쓸모있지도 않아 보이는 것들을 만들고 있지만, 일할 때 프로그래밍하는 것 보단 업무 이외로 하는게 점점 더 증가하고 있으니까요.

미래를 만드는 것을 제 손으로 올리고 하나씩 만들어 나가는 중이고 앞으로도 계속 하다보면 정말 좋은 미래가 올 것 같은 예감이 듭니다.

P.S 전 책 표지가 꽤 멋지다고 생각되서 편집 디자인을 하는 아내에게 보여줬더니 반응이 시큰둥하네요. 광고쪽에서는 저런 디자인이 별로 안먹히나 봅니다.

MAUI build 이후 디버그나 실행되지 않을 때 – code 2147942405 (0x80070005)

react-electron 이나 vue-electron 을 사용하여 개발하려 하였으나 fs 모듈이나 path 모듈 사용 제한을 해결하는 것이 영 껄끄럽기도 하고, 수많은 파편들을 정리해서 팀원들에게 전달하는 것도 비효율적이라고 판단하여 깔끔히 포기했다.

그래서 대안으로 생각한 것이 MAUI 이다. 문제는 이것도 아직 안정화되었다고 보기 어려운 것이라 생각치도 못한 오류를 내포하고 있다.

Windows 환경에서 .net 7.0 – MAUI 기반 프로젝트를 생성하고 디버그/릴리즈 모드에서 실행할 때 code 2147942405 (0x80070005) 와 함께 바로 종료되어 버린다. (웃기는 건 mac에서는 잘 된다.)

나만 그런게 아니라 세계 많은 프로그래머들이 부딛힌 이슈다.

MAUI apps crash on launch on Windows after Visual Studio update – code 2147942405 (0x80070005) #12080

해결 방법은 다음과 같다.

*.csproj 파일의 <PropertyGroup> 하위 엘리먼트로 <WindowsAppSdkDeploymentManagerInitialize>false</WindowsAppSdkDeploymentManagerInitialize>를 추가할 것.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
     <WindowsAppSdkDeploymentManagerInitialize>false</WindowsAppSdkDeploymentManagerInitialize>
    <TargetFrameworks>net7.0-android;net7.0-ios;net7.0-maccatalyst</TargetFrameworks>
....

Windows App SDK 1.2 redistributable 를 설치하고 나면 잘 동작한다는 사람들도 여럿 있으나, 필자 같은 경우는 위의 내용으로 충분히 정상 동작했다. 이미 보고된 이슈라고 하는데, Visual studio 업데이트나 배포시 포함시켜주면 좋겠지만… 분하다 MS.

vubell 입력 폼 개발 (1)

진행하기 앞서 vuetify를 추가한다.

Vuetify는 vue 기반의 UI Framework으로 <v-btn>과 같이 사전 정의된 컴포넌트 및 레이아웃 라이브러리이다.

https://next.vuetifyjs.com/

Vuetify is a no design skills required UI Framework with beautifully handcrafted Vue Components.
cd vubell
yarn add vuetify@^3.0.6
#vuetify 공식 홈페이지에서 material design icons를 언급해서 여기에도 설치하여 사용한다.
yarn add @mdi/font -D

electron vue app의 기본 호출 순서는 다음과 같다. (물론 babel과 webpack의 컴파일 및 빌드 과정 및 결과 js를 로드하는 것이긴 해도 개념상으로는 아래를 참고하는 것이 좋다.)

  1. yarn electron:serve (package.json)
  2. vue.config.js
  3. src/main.js
  4. src/App.vue
  5. … (사용되는 컴포넌트)

vuetify를 사용하려면 당연히 import나 require를 사용하여 로드하여야 한다. 여기서는 App.vue를 호출하는 main.js에 추가하겠다.

src/main.js

import { createApp } from 'vue'
import App from './App.vue'
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { aliases, mdi } from 'vuetify/iconsets/mdi'

const vuetify = createVuetify({
  components, 
  directives,
  theme: {defaultTheme: 'dark'},
  icons: {
    defaultSet: 'mdi',
    aliases,
    sets: {
      mdi,
    }
  }
})

createApp(App).use(vuetify).mount('#app')

먼저 입력 폼을 컴포넌트로 만든다.

Connection 정보 입력 기본 양식 컴포넌트

components/ConnectionCard.vue

<template>
  <v-card>
    <v-form>
      <v-card-text>Connection - SSH</v-card-text>
      <v-card-item>
        <v-row>
          <v-col cols="4">
            <v-text-field label="Alias"></v-text-field>
          </v-col>
          <v-col cols="6">
            <v-text-field label="Host Name (or IP address)"></v-text-field>
          </v-col>
          <v-col cols="2">
            <v-text-field label="port" type="number"></v-text-field>
          </v-col>
        </v-row>
        <v-row>
          <v-col cols="6">
            <v-text-field label="User ID"></v-text-field>
          </v-col>
          <v-col cols="6">
            <v-text-field label="Password" type="password"></v-text-field>
          </v-col>
        </v-row>
      </v-card-item>
    </v-form>
  </v-card>
</template>

<script>
export default {
  name: 'ConnectionCard',
}
</script>

Hello world를 보여주고 있는 App.vue를 수정하여 기본 레이아웃을 구성한다.

App.vue

<template>
  <v-app id="appRoot">
    <v-system-bar>
      <v-spacer></v-spacer>
      <v-icon>mdi-square</v-icon>
      <v-icon>mdi-circle</v-icon>
      <v-icon>mdi-triangle</v-icon>
    </v-system-bar>
    <v-navigation-drawer v-model="drawer">
      <v-sheet color="grey-lighten-4" class="pa-4">
        <v-avatar class="mb-4" color="grey-lighten-1" size="64"></v-avatar>
        <div>defree.inc@defree.co.kr</div>
      </v-sheet>
      <v-divider></v-divider>
      <v-list>
        <v-list-item v-for="[icon, text] in links" :key="icon" link>
          <template v-slot:prepend>
            <v-icon>{{ icon }}</v-icon>
          </template>
          <v-list-item-title>{{ text }}</v-list-item-title>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>
    <v-main>
      <v-container class="py-8 px-6" fluid>
        <ConnectionCard/>
      </v-container>
    </v-main>
  </v-app>
</template>

<script>
import ConnectionCard from './components/ConnectionCard.vue'
export default {
  components: {
    ConnectionCard
  },
  data: () => ({
    drawer: null,
    links: [
      ['mdi-inbox-arrow-down', 'Connection'],
      ['mdi-send', 'Hosts'],
      ['mdi-delete', 'Trash'],
      ['mdi-alert-octagon', 'Span'],
    ]
  }),
  methods: {
    loadProjectFile: function() {
    }
  },
  mounted: function() {
    this.loadProjectFile();
  }
}


</script>

지금까지 진행한 것을 확인해본다.

yarn electron:serve
dark 테마를 적용했다.

vue/cli “Error: The project seems to require yarn but it’s not installed”

2022.12.17 추가

왜 그런지는 정확히 모르겠지만 yarn serve 등 yarn을 실행할 때 cmd 에서 실행하면 오류가 발생하고 bash (msys나 git에 포함된 bash) 에서 실행하면 오류가 나지 않는다.

개요

nodejs, vue/cli, yarn을 활용하여 vue 프로젝트를 만들고 yarn으로 로컬 실행시 “Error: The project seems to require yarn but it’s not installed” 오류가 발생한다. 그에 대응한 방법을 기술한다.

환경

  • Windows 10
  • node v18.12.1
  • npm v8.19.2
  • yarn v1.22.19
  • vue
  • vue/cli v5.0.8

오류 재현

일반적인 vue 프로젝트를 생성하는 vue create [project name] 을 따른다.

vue create project-sample
#프리셋 선택이 있다. 여기서는 Default [Vue 3] babel, eslint 를 선택하였으나 다른 것을 선택해도 동일한 오류가 발생한다.
cd project-sample
yarn serve

그럼 아래와 같은 오류 로그가 출력된다.

$ vue-cli-service serve
 INFO  Starting development server...


 DONE  Compiled successfully in 5644ms                                                                      오후 1:17:22


  App running at:
  - Local:   http://localhost:8080/
  - Network: http://10.100.22.62:8080/

 ERROR  Error: The project seems to require yarn but it's not installed.
Error: The project seems to require yarn but it's not installed.
    at checkYarn (C:\src\node-workspace\vue1\node_modules\@vue\cli-shared-utils\lib\env.js:46:43)
    at exports.hasProjectYarn (C:\src\node-workspace\vue1\node_modules\@vue\cli-shared-utils\lib\env.js:42:10)
    at C:\src\node-workspace\vue1\node_modules\@vue\cli-service\lib\commands\serve.js:330:34
    at Hook.eval [as callAsync] (eval at create (C:\src\node-workspace\vue1\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:44:1)
    at Hook.CALL_ASYNC_DELEGATE [as _callAsync] (C:\src\node-workspace\vue1\node_modules\tapable\lib\Hook.js:18:14)
    at Watching._done (C:\src\node-workspace\vue1\node_modules\webpack\lib\Watching.js:287:28)
    at C:\src\node-workspace\vue1\node_modules\webpack\lib\Watching.js:209:21
    at Compiler.emitRecords (C:\src\node-workspace\vue1\node_modules\webpack\lib\Compiler.js:919:5)
    at C:\src\node-workspace\vue1\node_modules\webpack\lib\Watching.js:187:22
    at C:\src\node-workspace\vue1\node_modules\webpack\lib\Compiler.js:885:14
    at Hook.eval [as callAsync] (eval at create (C:\src\node-workspace\vue1\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:12:1)
    at Hook.CALL_ASYNC_DELEGATE [as _callAsync] (C:\src\node-workspace\vue1\node_modules\tapable\lib\Hook.js:18:14)
    at C:\src\node-workspace\vue1\node_modules\webpack\lib\Compiler.js:882:27
    at C:\src\node-workspace\vue1\node_modules\neo-async\async.js:2818:7
    at done (C:\src\node-workspace\vue1\node_modules\neo-async\async.js:3522:9)
    at Hook.eval [as callAsync] (eval at create (C:\src\node-workspace\vue1\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:6:1)
    at C:\src\node-workspace\vue1\node_modules\webpack\lib\Compiler.js:736:33
    at Immediate._onImmediate (C:\src\node-workspace\vue1\node_modules\memfs\lib\volume.js:701:13)
    at process.processImmediate (node:internal/timers:471:21)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

이는 vue의 cli-shared-utils 에서 yarn 버전을 확인하는 hasYarn 함수에서 오류가 나기 때문인데… 그 오류의 정확한 원인을 몰라 직접 해결할 수는 없었다.

실제로 yarn에 버그가 있었던 것도 아니고, 현재 환경에서 실행하는 것은 딱히 문제가 없었으므로 해당 함수를 수정하여 오류는 넘어갔다.

PROJECT_ROOT\node_modules\@vue\cli-shared-utils\lib\env.js 수정

// env detection
exports.hasYarn = () => {
  if (process.env.VUE_CLI_TEST) {
    return true
  }
  if (_hasYarn != null) {
    return _hasYarn
  }
  try {
    execSync('yarn --version', { stdio: 'ignore' })
    return (_hasYarn = true)
  } catch (e) {
    return (_hasYarn = false)
  }
}

위의 내용 중 catch하여 false를 리턴할 때 그냥 true를 리턴하도록 수정한다.

// env detection
exports.hasYarn = () => {
  if (process.env.VUE_CLI_TEST) {
    return true
  }
  if (_hasYarn != null) {
    return _hasYarn
  }
  try {
    execSync('yarn --version', { stdio: 'ignore' })
    return (_hasYarn = true)
  } catch (e) {
    return (_hasYarn = true) // 밑줄 쫙!
  }
}

이거 언제 수정되려나….

vubell 프로젝트 생성

필요 환경

  • node js
  • vue
  • vue/cli

환경

  • Windows 10
  • node v18.12.1
  • npm v8.19.2
  • yarn v1.22.19
  • vue
  • vue/cli v5.0.8

vue 프로젝트 생성

vue create vubell

세부 선택은 아래와 같다.

Vue CLI v5.0.8
? Please pick a preset: Default ([Vue 3] babel, eslint)

electron-builder 설치 (vue-cli-plugin-electron-builder)

cd vubell
vue add electron-builder@alpha
#electron version은 16을 선택했다.
Choose Electron Version ^16.0.0

vue-cli-plugin-electron-builder는 vue와 electron을 연동하기 위한 package.json, module 등을 자동으로 추가해 주는 plugin이다.

https://nklayman.github.io/vue-cli-plugin-electron-builder/

문제는, 2022.12.17 electron-builder의 버전은 2.0.0인데 이는 node 17 버전까지만 지원하여 설치할 때 오류가 발생한다. 다행히 3.0.0 alpha 는 node 18을 지원하여 alpha 버전이라도 설치하여 진행한다.

설치가 완료되면 package.json의 scripts 영역에

    "electron:build": "vue-cli-service electron:build",
    "electron:serve": "vue-cli-service electron:serve",

가 추가된 것을 볼 수 있다. yarn electron:serve를 실행하면

vue-dev-tools 까지 설치되어 있다구!

혹시라도 “Error: The project seems to require yarn but it’s not installed” 오류가 발생한다면 http://blog.defree.co.kr/?p=79 를 참고하길 바란다.


프로그램을 종료해도 node 프로세스가 계속 살아있어..

vubell(electron)을 종료해도, command에서 Ctrl+c 로 종료해도 node.exe 프로세스가 살아있고 해당 프로젝트 디렉토리와 파일에 파일 핸들을 갖고 있는 상태일 때가 있다. 그래서 프로젝트 디렉토리를 지우거나 이름을 변경할 수 없게 된다.

이럴 땐 command 창에서 아래의 명령어를 입력해보자.

taskkill /f /im node.exe

fmap tailwind 적용

tailwind 설치 (+ postcss, autoprefixer)

tailwind

css framework. https://tailwindcss.com/

postcss

css와 javascript의 결합을 지원하는 라이브러리

autoprefixer

vender-prefix css 자동 적용 라이브러리


일단 라이브러리를 설치한다.

npm install -D tailwindcss postcss autoprefixer

npx로 tailwind 초기화. 그럼 tailwind.config.js 를 생성한다.

npx tailwindcss init

tailwind.config.js 파일은 본 프로젝트에 적용할 tailwind의 기본 css 값들을 설정한다. 초기 생성된 내용은 아래와 같다.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [],
  theme: {
    extend: {},
  },
  plugins: [],
}

module.exports.content 부분에 tailwind가 탐색할 html, js, jsx 등의 적용 리소스 파일을 기술한다. 실제 적용한 내용은 아래와 같다.

/** @type {import('tailwindcss').Config} */
const colors = require('tailwindcss/colors')

module.exports = {
  purge: ['./public/**/*.html'],
  content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
  darkMode: 'class', // or 'media' or false
  theme: {
    fontFamily: {
      sans: ['cairo', 'sans-serif'],
    },
    extend: {
      colors: {
        light: 'var(--light)',
        dark: 'var(--dark)',
        darker: 'var(--darker)',
        primary: {
          DEFAULT: 'var(--color-primary)',
          50: 'var(--color-primary-50)',
          100: 'var(--color-primary-100)',
          light: 'var(--color-primary-light)',
          lighter: 'var(--color-primary-lighter)',
          dark: 'var(--color-primary-dark)',
          darker: 'var(--color-primary-darker)',
        },
        secondary: {
          DEFAULT: colors.fuchsia[600],
          50: colors.fuchsia[50],
          100: colors.fuchsia[100],
          light: colors.fuchsia[500],
          lighter: colors.fuchsia[400],
          dark: colors.fuchsia[700],
          darker: colors.fuchsia[800],
        },
        success: {
          DEFAULT: colors.green[600],
          50: colors.green[50],
          100: colors.green[100],
          light: colors.green[500],
          lighter: colors.green[400],
          dark: colors.green[700],
          darker: colors.green[800],
        },
        warning: {
          DEFAULT: colors.orange[600],
          50: colors.orange[50],
          100: colors.orange[100],
          light: colors.orange[500],
          lighter: colors.orange[400],
          dark: colors.orange[700],
          darker: colors.orange[800],
        },
        danger: {
          DEFAULT: colors.red[600],
          50: colors.red[50],
          100: colors.red[100],
          light: colors.red[500],
          lighter: colors.red[400],
          dark: colors.red[700],
          darker: colors.red[800],
        },
        info: {
          DEFAULT: colors.cyan[600],
          50: colors.cyan[50],
          100: colors.cyan[100],
          light: colors.cyan[500],
          lighter: colors.cyan[400],
          dark: colors.cyan[700],
          darker: colors.cyan[800],
        },
      },
    },
  },
  variants: {
    extend: {
      backgroundColor: ['checked', 'disabled'],
      opacity: ['dark'],
      overflow: ['hover'],
    },
  },
  plugins: [],
}

tailwind는 tailwind의 규칙에 따라서 작성된 css 파일을 컴파일하여 실제 html, js에 적용할 css를 생성한다.

npx tailwindcss -i ./src/tailwind.css -p ./public/fmap.css

그래서 실제 작성한(컴파일 전) css 파일을 아래와 같다.

:root {
  --light: #edf2f9;
  --dark: #152e4d;
  --darker: #12263f;
  /*  */
  --color-primary: var(--color-cyan);
  --color-primary-50: var(--color-cyan-50);
  --color-primary-100: var(--color-cyan-100);
  --color-primary-light: var(--color-cyan-light);
  --color-primary-lighter: var(--color-cyan-lighter);
  --color-primary-dark: var(--color-cyan-dark);
  --color-primary-darker: var(--color-cyan-darker);
  /*  */
  --color-green: #16a34a;
  --color-green-50: #f0fdf4;
  --color-green-100: #dcfce7;
  --color-green-light: #22c55e;
  --color-green-lighter: #4ade80;
  --color-green-dark: #15803d;
  --color-green-darker: #166534;
  /*  */
  --color-blue: #2563eb;
  --color-blue-50: #eff6ff;
  --color-blue-100: #dbeafe;
  --color-blue-light: #3b82f6;
  --color-blue-lighter: #60a5fa;
  --color-blue-dark: #1d4ed8;
  --color-blue-darker: #1e40af;
  /*  */
  --color-cyan: #0891b2;
  --color-cyan-50: #ecfeff;
  --color-cyan-100: #cffafe;
  --color-cyan-light: #06b6d4;
  --color-cyan-lighter: #22d3ee;
  --color-cyan-dark: #0e7490;
  --color-cyan-darker: #155e75;
  /*  */
  --color-teal: #0d9488;
  --color-teal-50: #f0fdfa;
  --color-teal-100: #ccfbf1;
  --color-teal-light: #14b8a6;
  --color-teal-lighter: #2dd4bf;
  --color-teal-dark: #0f766e;
  --color-teal-darker: #115e59;
  /*  */
  --color-fuchsia: #c026d3;
  --color-fuchsia-50: #fdf4ff;
  --color-fuchsia-100: #fae8ff;
  --color-fuchsia-light: #d946ef;
  --color-fuchsia-lighter: #e879f9;
  --color-fuchsia-dark: #a21caf;
  --color-fuchsia-darker: #86198f;
  /*  */
  --color-violet: #7c3aed;
  --color-violet-50: #f5f3ff;
  --color-violet-100: #ede9fe;
  --color-violet-light: #8b5cf6;
  --color-violet-lighter: #a78bfa;
  --color-violet-dark: #6d28d9;
  --color-violet-darker: #5b21b6;
}

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .toggle:checked + span {
    @apply top-0 left-6 bg-primary;
  }
  .toggle:disabled + span {
    @apply bg-gray-500 shadow-none;
  }
  .toggle:checked:disabled {
    @apply bg-gray-200;
  }
  .toggle:checked:disabled + span {
    @apply shadow-none bg-primary-lighter;
  }
  .toggle:focus + span {
    @apply ring ring-primary-lighter;
  }

  input:invalid.invalid,
  textarea:invalid.invalid {
    @apply ring ring-danger-light dark:ring-danger;
  }
}

생성된 css 파일은 너무 길어서 넘어간다.

fmap react-router 기본 구성 (jsx 스타일)

router 패키지 추가

yarn add react-router-dom
yarn add --dev cross-env

페이지 1 : src/pages/Project.js

function Project() {
    return (
        <div>
            <div>project setting & loading</div>
        </div>
    );
}

export default Project;

페이지 2 : src/pages/Host.js

function Host() {
  return (
    <div>
      <form>
        <label For="address">address</label>
        <input name="address"></input>
        <label For="port">port</label>
        <input name="port"></input>
      </form>
    </div>
  );
}

export default Host;

페이지 3 : src/pages/HostAnalysis.js

function HostAnalysis() {
    return (
        <div>host: </div>
    );
}

export default HostAnalysis;

Navigation : src/pages/Root.js

import React from "react";
import {Link, Route, BrowserRouter, Routes, Outlet} from "react-router-dom";

export default function Root() {
  return (
    <div>
      <Link to="/projects">
        <button>Projects</button>
      </Link>
      <Link to="/host">
        <button>Host</button>
      </Link>
      <Link to="/hostAnalysis">
        <button>HostAnalysis</button>
      </Link>
      <Outlet/>
    </div>
  );
};

에러 페이지 : src/pages/NotFound.js

import { useRouteError, Link, Routes, Route, BrowserRouter } from "react-router-dom";
import Root from "./Root";

function NotFound() {
  const error = useRouteError();
  console.error(error);

  return (
    <BrowserRouter>
  <div id="not-found-root">
      <h1>Oops!</h1>
      <p>Sorry, an unexpected error has occured.</p>
      <p><i>{error.statusText || error.message}</i></p>
    </div>
    <Routes>
      <Link to="/">go home.</Link>
      <Route path="/" component={<Root/>}>root</Route>
    </Routes>
    </BrowserRouter>
  );
}

export default NotFound;

Route : index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import {
  createBrowserRouter
  , RouterProvider
  , Route
  , createRoutesFromElements
  , BrowserRouter
} from "react-router-dom"
import Root from "./pages/Root";
import NotFound from './pages/NotFound';
import App from './App';
import Project from "./pages/Project";
import Host from "./pages/Host";
import HostAnalysis from "./pages/HostAnalysis";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root/>} errorElement={<NotFound/>}>
      <Route errorElement={<NotFound/>}>
        <Route path="/projects" element={<Project/>}></Route>
        <Route path="/host" element={<Host/>}></Route>
        <Route path="/hostAnalysis" element={<HostAnalysis/>}></Route>
      </Route>
    </Route>
  )
);

const documentBodyRoot = ReactDOM.createRoot(document.getElementById('root'));
documentBodyRoot.render(
  <React.StrictMode>
    <RouterProvider router={router}></RouterProvider>
  </React.StrictMode>
);

fmap react-router 기본 구성

router 패키지 추가

yarn add react-router-dom
yarn add --dev cross-env

src/Project.js

function Project() {
    return (
        <div>
            <div>project setting & loading</div>
        </div>
    );
}

export default Project;

src/Host.js

function Host() {
  return (
    <div>
      <form>
        <label for="address">address</label>
        <input name="address"></input>
        <label for="port">port</label>
        <input name="port"></input>
      </form>
    </div>
  );
}

export default Host;

src/HostAnalysis.js

function HostAnalysis() {
    return (
        <div>host: </div>
    );
}

export default HostAnalysis;

src/App.js

import React from "react";
import {Link, Route, BrowserRouter, Routes} from "react-router-dom";
import Host from "./pages/Host";
import HostAnalysis from "./pages/HostAnalysis";
import Project from "./pages/Project";

function App() {
  return (
    <BrowserRouter>
        <Link to="/">
          <button>Projects</button>
        </Link>
        <Link to="/host">
          <button>Host</button>
        </Link>
        <Link to="/hostAnalysis">
          <button>HostAnalysis</button>
        </Link>
      <Routes>
        <Route path="/" component={Project}></Route>
        <Route path="/host" component={Host}></Route>
        <Route path="/hostAnalysis" component={HostAnalysis}></Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;
« Older posts

© 2025 Samuel's journals

Theme by Anders NorenUp ↑