[월:] 2022년 12월

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>
);

© 2025 Samuel's journals

Theme by Anders NorenUp ↑