가상 keyboard 만들기

첫 프로젝트로 가상 키보드를 만들어 보자. 웹팩을 활용해서 개발환경을 설정하고, HTML과 CSS를 이용해 키보드 레이아웃을 하고, 다크 테마 기능과 폰트 변경 기능도 추가 해보자. 그리고 마지막으로 키 이벤트를 이용해 실제 키보드를 눌렀을 때 어떤 키가 눌러지는지 화면상으로 보여주도록 하자. 그리고 마우스 이벤트를 이용해서도 키 컨트롤을 눌렀을 때 입력이 가능하도록 하자.

웹팩 설정

npm init –y
npm i –D webpack webpack-cli webpack-dev-server
module.export = {
    entry: "./src/js/index.js",
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "./docs"),
        clean: true
    },
    devtool: "source-map",
    mode: "development",
    devServer: {
        host: "localhost",
        port: 8080,
        open: true,
        watchFiles: 'index.html'
    },
    plugins:[
        new HtmlWebpackPlugin({
            title: "keyboard",
            template: "./index.html",
            inject: "body",
            favicon: "./favicon.PNG"
        }),
        new MiniCssExtractPlugin({
            filename:"style.css"
        })
    ],
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, "css-loader"]
            }
          ]
    },
    optimization: {
        minimizer: [
            new TerserWebpackPlugin(),
            new CssMinimizerPlugin()
        ]
    }
}
npm i -D html-webpack-plugin
npm i -D mini-css-extract-plugin css-loader css-minimizer-webpack-plugin

ESLint & Prittier

npm i -D eslint
npm i -D --save-exact prettier
npm i -D eslint-config-prettier eslint-plugin-prettier
npx eslint --init
{
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:prettier/recommended"
    ],
}
/node_modules
/docs
webpack.config.js
{
    "arrowParens": "always",
    "bracketSameLine": false,
    "bracketSpacing": true,
    "embeddedLanguageFormatting": "auto",
    "htmlWhitespaceSensitivity": "css",
    "insertPragma": false,
    "jsxSingleQuote": false,
    "printWidth": 80,
    "proseWrap": "preserve",
    "quoteProps": "as-needed",
    "requirePragma": false,
    "semi": true,
    "singleAttributePerLine": false,
    "singleQuote": false,
    "tabWidth": 2,
    "trailingComma": "es5",
    "useTabs": false,
    "vueIndentScriptAndStyle": false
  }
{
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    }
}

HTML

 <div class="row">
                <div class="key" data-code="Backquote" data-val="`">
                    <span class="two-value">~</span>
                    <span class="two-value">`</span>
                </div>
                <div class="key" data-code="Digit1" data-val="1">
                    <span class="two-value">!</span>
                    <span class="two-value">1</span>
                </div>
<div>
.row {
    display: flex;
  }
.key {
    width: 60px;
    height: 60px;
    margin: 5px;
    border-radius: 4px;
    background-color: white;
    cursor: pointer;
    /* 하나의 키에 위 아래로 두개가 입력 가능한 경우 */
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    transition: 0.2s;
}
<label class="switch">
    <input id="switch" type="checkbox">
    <span class="slider"></span>
</label>
.slider::before {
    position: absolute;
    content: "";
    height: 26px;
    width: 26px;
    left: 4px;
    bottom: 4px;
    background-color: white;
    transition: 0.5s;
    border-radius: 50%;
}
input:checked + .slider::before {
    transform: translateX(26px);
}

Keyboard.js

#assignElement() {
    this.#switchEl = document.getElementById('switch');
    this.#fontSelectEl = document.getElementById('font');
    this.#keyboardEl = document.getElementById('keyboard');
    this.#inputGroupEl = document.getElementById('input-group');
    this.#inputEl = document.getElementById('input');
}
#addEvent() {
    this.#switchEl.addEventListener('change', this.#onChangeTheme);
    this.#fontSelectEl.addEventListener('change', this.#onChangeFont);
    this.#inputEl.addEventListener('input', this.#onInput.bind(this));
    document.addEventListener('keydown', this.#onKeyDown.bind(this));
    document.addEventListener('keyup', this.#onKeyUp.bind(this));
    this.#keyboardEl.addEventListener(
      'mousedown',
      this.#onMouseDown.bind(this)
    );
    document.addEventListener('mouseup', this.#onMouseUp.bind(this));
}
#onChangeTheme(ev) {
    document.documentElement.setAttribute(
      'theme',
      ev.target.checked ? 'dark-mode' : ''
    );
}
html[theme="dark-mode"] {
    filter: invert(100%) hue-rotate(180deg);
  }
.key.active {
    background-color: #333;
    color: #fff;
}
#onKeyDown(ev) {
    if (this.#mouseDown) return;
    this.#keyPress = true;
    this.#keyboardEl
      .querySelector(`[data-code=${ev.code}]`)
      ?.classList.add('active');
}
  #onKeyUp(ev) {
    if (this.#mouseDown) return;
    this.#keyPress = false;
    this.#keyboardEl
      .querySelector(`[data-code=${ev.code}]`)
      ?.classList.remove('active');
}
#onInput(ev) {
    if (this.#mouseDown) return;
    ev.target.value = ev.target.value.replace(/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/, '');
    this.#inputGroupEl.classList.toggle(
      'error',
      /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(ev.data)
    );
}
#onMouseDown(ev) {
    if (this.#keyPress) return;
    this.#mouseDown = true;
    ev.target.closest('div.key')?.classList.add('active');
}
#onMouseUp(ev) {
    if (this.#keyPress) return;
    this.#mouseDown = false;
    const keyEl = ev.target.closest('div.key');
    const isActive = !!keyEl?.classList.contains('active');
    const val = keyEl?.dataset.val;
    if (isActive && !!val && val !== 'Space' && val !== 'Backspace') {
      this.#inputEl.value += val;
    }
    // Space
    if (isActive && val === 'Space') {
      this.#inputEl.value += ' ';
    }
    // Backspace
    if (isActive && val === 'Backspace') {
      this.#inputEl.value = this.#inputEl.value.slice(0, -1);
    }
    this.#keyboardEl.querySelector('.active')?.classList.remove('active');
}
document.addEventListener('mouseup', this.#onMouseUp.bind(this));

끝!