[react 30] #1 가상 키보드 구현하기

dev__bokyoung·2022년 7월 2일
6
post-thumbnail

프롤로그

패스트캠퍼스에 [30개 프로젝트로 배우는 프론트엔드] 라는 강의가 있어서 듣기 시작했다. 클론코딩인데 듣고 끝! 하게 되면 사실상 나중에 기억이 하나도 남게 되지 않으니까 1프로젝트 1글 정리를 하고자 한다. 첫번째는 가상 키보드 구현하기이다.

webpack 이용한 개발환경 구축

module bundler
웹개발에 필요한 html,css,javascript 등을 하나의 파일 또는 여러개의 파일로 병합하거나 압축해주는 역할을 한다.

1. package.json 초기화

npm init -y

2. webpack 관련 패키지 설치

npm i -D webpack webpack-cli webpack-dev-server

-D 는 dev dependancies로써 설치를 하겠다는 뜻이고
local 개발이나 test 를 설치하는데에만 쓰이는 패키지를 뜻한다.

반면 dependacies 로 설치를 하게 되면
production 환경에서 필요한 패키지를 뜻한다.

3. src 폴더 생성

개발할 때 필요한 파일들을 생성 해 준다.

4. 추가 플러그인 설치

npm i -D terser-webpack-plugin //압축 플러그인 
npm i -D html-webpack-plugin //html 관련 모듈
npm i -D mini-css-extract-plugin css-loader css-minimizer-webpack-plugin //css 관련 모듈

5. webpack.config.js 세팅

각각 주석으로 해당에 관한 설명을 적어놨다.

const path = require("path");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const HtmlwebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  entry: "./src/js/index.js", //js 파일의 진입점
  output: {
    // 빌드를 했을때 번들파일 관련 속성
    filename: "bundle.js", // 파일이름 지정(번들들파일)
    path: path.resolve(__dirname, "./dist"), //번들파일이 생성될 경로, path.resolve 메소드를 사용해서 __dirname 을사용해 웹팩이 절대경로를 찾을 수 있도록 해줌
    clean: true, //이미 번들파일이 있다면 다 지우고 다시 만들어주는 속성
  },
  devtool: "source-map", //build 한 파일과 원본 파일을 연결시켜주는 파일
  mode: "development", //production 과 devlopment 모드가 있는데 html,css,js 파일을 난독화 기능을 제공하는지에 대한 차이
  devServer:{
    host:"localhost",
    port:8080,
    open:true, // dev 서버를 열때 새창을 이용해서 열어줘라 
    watchFiles: "index.html" //html 변화 감지를 지켜봄, 변화가 있을때마다 reload
  },
  plugins: [
    new HtmlwebpackPlugin({
      title: "keyboard", //title
      template: "./index.html", //lodash 파일 사용할 수 있게 해줌 -> 유틸성 메소드나 템플릿성 메소드를 제공해 주는 라이브러리
      inject: "body", //js 번들했을때 파일을 body 쪽에 넣어주겠다.
      favicon: "./favicon.ico",
    }),
    new MiniCssExtractPlugin({
      filename: "style.css",
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"], //css파일을 이런 로더를 사용해서 읽어들이도록 하겠다.
      },
    ],
  },

  optimization: {
    //압축해주는 친구들
    minimizer: [new TerserWebpackPlugin(), new CssMinimizerPlugin()],
  },
};

6. index.html 파일 세팅

HtmlwebpackPlugin 플러그인을 설치하고, template: "./index.html" template 를 index.html 로 설정해 주었다. 이렇게 하면 lodash 파일을 사용할 수 있게 해주는데 index.html 파일에 따로 lodash 문법을 사용해서 적어줘야한다. header 안에 적어줬다.


  <head>
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>

7. eslint & prettier

  • eslint 는 js linter 중에 하나로 정적 분석을 통해 문법적 오류를 찾아준다. 간단한 포맷팅 기능도 제공한다.
  • prettier 는 코드 포맷팅 중에 하나이다.
  • ^ 캐럿 표시는 npm 설치 시 마이너 버전이 업데이트가 되었으면 마이너 버전까지는 업데이트를 허용한다라는 뜻이다. (--save-exact 사용하여 설치)
  • eslint 가 포맷팅 기능도 제공하다 보니까 prettier 가 formatting 이 겹치는 룰이 있어 충돌이 나게 된다. 그 충돌을 방지하기 위해 eslint-config-prettier 플러그인 추가해 준다.
  • eslint-plugin-prettier 는 eslint에 prettier 플러그인을 추가해 주기 위한 패키지이다.
npm i -D eslint
npm install --save-dev --save-exact prettier 

npm i -D eslint-config-prettier eslint-plugin-prettier

.eslintrc.json

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": ["eslint:recommended", "plugin:prettier/recommended"],
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "rules": {
    "prettier/prettier": "error"
  }
}

.eslintignore

/node_modules
/dis
webpack.config.js

.prettierrc.json

prettier 홈페이지의 기본 reccommnet 를 가져왔다.

{
  "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,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "es5",
  "useTabs": false,
  "vueIndentScriptAndStyle": false
}

.prettierignore

/node_modules
/dis
webpack.config.js

8. 최종 package.json

{
  "name": "playground",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --mode=production",
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^6.5.1",
    "css-minimizer-webpack-plugin": "^3.3.1",
    "eslint": "^8.18.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "html-webpack-plugin": "^5.5.0",
    "mini-css-extract-plugin": "^2.4.6",
    "prettier": "2.5.1",
    "terser-webpack-plugin": "^5.3.0",
    "webpack": "^5.65.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.7.2"
  }
}

html & css 작성

html <body> 에 들어 갈 내용

<body>
    <div class="container" id="container">
        <div class="menu">
            <label class="switch">
                <input id="switch" type="checkbox">
                <span class="slider"></span>
            </label>
            <div class="select-box">
                <label for="font">Font:</label>
                <select id="font">
                    <option value="" disabled selected>Choose Font</option>
                    <option value="Comic Sans MS, Comic Sans, cursive">Font 1</option>
                    <option value="Arial Narrow, sans-serif">Font 2</option>
                    <option value="Chalkduster, fantasy">Font 3</option>
                </select>
            </div>
        </div>
        <div class="input-group" id="input-group">
            <input id="input" class="input" type="text" autocomplete="off">
            <div class="error-message">한글 입력 불가</div>
        </div>
        <div class="keyboard" id="keyboard">
            <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 class="key" data-code="Digit2" data-val="2">
                    <span class="two-value">@</span>
                    <span class="two-value">2</span>
                </div>
                <div class="key" data-code="Digit3" data-val="3">
                    <span class="two-value">#</span>
                    <span class="two-value">3</span>
                </div>
                <div class="key" data-code="Digit4" data-val="4">
                    <span class="two-value">$</span>
                    <span class="two-value">4</span>
                </div>
                <div class="key" data-code="Digit5" data-val="5">
                    <span class="two-value">%</span>
                    <span class="two-value">5</span>
                </div>
                <div class="key" data-code="Digit6" data-val="6">
                    <span class="two-value">^</span>
                    <span class="two-value">6</span>
                </div>
                <div class="key" data-code="Digit7" data-val="7">
                    <span class="two-value">&</span>
                    <span class="two-value">7</span>
                </div>
                <div class="key" data-code="Digit8" data-val="8">
                    <span class="two-value">*</span>
                    <span class="two-value">8</span>
                </div>
                <div class="key" data-code="Digit9" data-val="9">
                    <span class="two-value">(</span>
                    <span class="two-value">9</span>
                </div>
                <div class="key" data-code="Digit0" data-val="0">
                    <span class="two-value">)</span>
                    <span class="two-value">0</span>
                </div>
                <div class="key" data-code="Minus" data-val="-">
                    <span class="two-value">_</span>
                    <span class="two-value">-</span>
                </div>
                <div class="key" data-code="Equal" data-val="=">
                    <span class="two-value">+</span>
                    <span class="two-value">=</span>
                </div>
                <div class="key back-space-key" data-code="Backspace" data-val="Backspace">
                    Backspace
                </div>
            </div>
            <div class="row">
                <div class="key tab-key">Tab</div>
                <div class="key" data-code="KeyQ" data-val="q">Q</div>
                <div class="key" data-code="KeyW" data-val="w">W</div>
                <div class="key" data-code="KeyE" data-val="e">E</div>
                <div class="key" data-code="KeyR" data-val="r">R</div>
                <div class="key" data-code="KeyT" data-val="t">T</div>
                <div class="key" data-code="KeyY" data-val="y">Y</div>
                <div class="key" data-code="KeyU" data-val="u">U</div>
                <div class="key" data-code="KeyI" data-val="i">I</div>
                <div class="key" data-code="KeyO" data-val="o">O</div>
                <div class="key" data-code="KeyP" data-val="p">P</div>
                <div class="key" data-code="BracketLeft" data-val="[">
                    <span class="two-value">{</span>
                    <span class="two-value">[</span>
                </div>
                <div class="key" data-code="BracketRight" data-val="]">
                    <span class="two-value">}</span>
                    <span class="two-value">]</span>
                </div>
                <div class="key back-slash-key" data-code="Backslash" data-val="\">
                    <span class="two-value">|</span>
                    <span class="two-value">\</span>
                </div>
            </div>
            <div class="row">
                <div class="key caps-lock-key">CapsLock</div>
                <div class="key" data-code="KeyA" data-val="a">A</div>
                <div class="key" data-code="KeyS" data-val="s">S</div>
                <div class="key" data-code="KeyD" data-val="d">D</div>
                <div class="key" data-code="KeyF" data-val="f">F</div>
                <div class="key" data-code="KeyG" data-val="g">G</div>
                <div class="key" data-code="KeyH" data-val="h">H</div>
                <div class="key" data-code="KeyJ" data-val="j">J</div>
                <div class="key" data-code="KeyK" data-val="k">K</div>
                <div class="key" data-code="KeyL" data-val="l">L</div>
                <div class="key" data-code="Semicolon" data-val=";">
                    <span class="two-value">:</span>
                    <span class="two-value">;</span>
                </div>
                <div class="key" data-code="Quote" data-val="'">
                    <span class="two-value">"</span>
                    <span class="two-value">'</span>
                </div>
                <div class="key enter-key" data-code="Enter">Enter</div>
            </div>
            <div class="row">
                <div class="key left-shift-key" data-code="ShiftLeft">Shift</div>
                <div class="key" data-code="KeyZ" data-val="z">Z</div>
                <div class="key" data-code="KeyX" data-val="x">X</div>
                <div class="key" data-code="KeyC" data-val="c">C</div>
                <div class="key" data-code="KeyV" data-val="v">V</div>
                <div class="key" data-code="KeyB" data-val="b">B</div>
                <div class="key" data-code="KeyN" data-val="n">N</div>
                <div class="key" data-code="KeyM" data-val="m">M</div>
                <div class="key" data-code="Comma" data-val=",">
                    <span class="two-value">
                        &lt;
                    </span>
                    <span class="two-value">,</span>
                </div>
                <div class="key" data-code="Period" data-val=".">
                    <span class="two-value">
                        &gt;
                    </span>
                    <span class="two-value">.</span>
                </div>
                <div class="key" data-code="Slash" data-val="/">
                    <span class="two-value">?</span>
                    <span class="two-value">/</span>
                </div>
                <div class="key right-shift-key" data-code="ShiftRight">Shift</div>
            </div>
            <div class="row">
                <div class="key fn-key">Ctrl</div>
                <div class="key fn-key">-</div>
                <div class="key fn-key">Alt</div>
                <div class="key space-key" data-code="Space" data-val="Space">Space</div>
                <div class="key fn-key">Alt</div>
                <div class="key fn-key">Fn</div>
                <div class="key fn-key">-</div>
                <div class="key fn-key">Ctrl</div>
            </div>
        </div>

    </div>
</body>
  

css 파일

* {
  user-select: none;
  outline: none;
}

html[theme="dark-mode"] {
  /* ! */
  filter: invert(100%) hue-rotate(180deg);
}

body {
  background-color: white;
}

.container {
  width: 1050px;
  margin: auto;
}

.keyboard {
  background-color: gray;
  color: gray;
  width: 1050px;
  border-radius: 4px;
}

.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;
  /* ! */
}

.key:hover {
  background-color: lightgray;
}

.key.active {
  background-color: #333;
  color: #fff;
}

.key .two-value {
  width: 100%;
  text-align: center;
}

.fn-key {
  width: 80px;
}

.space-key {
  width: 420px;
}

.back-space-key {
  width: 130px;
}

.tab-key {
  width: 95px;
}

.back-slash-key {
  width: 95px;
}

.caps-lock-key {
  width: 110px;
}

.left-shift-key {
  width: 145px;
}

.enter-key {
  width: 150px;
}

.right-shift-key {
  width: 185px;
}

.menu {
  /* ! */
  display: flex;
}

.switch {
  position: relative;
  width: 60px;
  height: 34px;
}

.switch input {
  display: none;
}

.slider {
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  position: absolute;
  cursor: pointer;
  background-color: gray;
  border-radius: 34px;
  transition: 0.4s;
}

/* ! */
.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 {
  background-color: black;
}

input:checked + .slider::before {
  /* ! */
  transform: translateX(26px);
}

.select-box {
  position: relative;
  margin-left: 60px;
  height: 34px;
}

.select-box select {
  /* ! */
  font-size: 0.9rem;
  /* ! */
  padding: 2px 5px;
  height: 34px;
  width: 200px;
}

.input-group {
  margin: 100px 0px;
}

.input {
  border: none;
  border-bottom: 2px solid lightgrey;
  width: 1050px;
  height: 50px;
  font-size: 30px;
  text-align: center;
  display: block;
}

.error-message {
  color: #cc0033;
  font-size: 30px;
  line-height: 30px;
  margin-top: 10px;
  text-align: center;
}

.input-group .error-message {
  display: none;
}

.error input {
  border-bottom: 2px solid red;
}

.error .error-message {
  display: block;
}

이벤트 스크립트

javascript 클래스로 만들어주었다.

이벤트 목록
1. 다크 모드 테마
2. 폰트 변경
3. 키보드로 작성 가능 (keydown, keyup)
4. 화면 가상 키보드 클릭 시 작성 가능 (mousedown, mouseup)

private class field

class 의 속성(property)들은 기본적으로 public 하며 class 외부에서 읽히고 수정될 수 있다. 하지만, ES2019 에서는 해쉬 # prefix 를 추가해 private class 필드를 선언할 수 있게 되었다.

export class Keyboard {
  #swichEl;
  #fontSelectEl;
  #containerEl;
  #keyboardEl;
  #inputGroupEl;
  #inputEl;
  #keyPress = false;
  #mouseDown = false;
  constructor() {
    this.#assignElement();
    this.#addEvent();
  }
  #assignElement() {
    this.#containerEl = document.getElementById("container");
    this.#swichEl = this.#containerEl.querySelector("#switch");
    this.#fontSelectEl = this.#containerEl.querySelector("#font");
    this.#keyboardEl = this.#containerEl.querySelector("#keyboard");
    this.#inputGroupEl = this.#containerEl.querySelector("#input-group");
    this.#inputEl = this.#inputGroupEl.querySelector("#input");
  }

  #addEvent() {
    this.#swichEl.addEventListener("change", this.#onChageTheme);
    this.#fontSelectEl.addEventListener("change", this.#onChageFont);
    document.addEventListener("keydown", this.#onKeyDown.bind(this));
    document.addEventListener("keyup", this.#onKeyUp.bind(this));
    this.#inputEl.addEventListener("input", this.#onInput);
    this.#keyboardEl.addEventListener(
      "mousedown",
      this.#onMouseDown.bind(this)
    );
    document.addEventListener("mouseup", this.#onMouseUp.bind(this));
  }

  #onMouseUp(event) {
    if (this.#keyPress) return;
    this.#mouseDown = true;
    const keyEl = event.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;
    }
    if (isActive && val === "Space") {
      this.#inputEl.value += " ";
    }
    if (isActive && val === "Backspace") {
      this.#inputEl.value = this.#inputEl.value.slice(0, -1);
    }

    this.#keyboardEl.querySelector(".active")?.classList.remove("active");
  }
  #onMouseDown(event) {
    if (this.#keyPress) return;
    this.#mouseDown = true;
    event.target.closest("div.key")?.classList.add("active");
  }
  #onInput(event) {
    event.target.value = event.target.value.replace(/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/, "");
  }
  #onKeyDown(event) {
    if (this.#mouseDown) return;
    this.#keyPress = true;
    this.#keyboardEl
      .querySelector(`[data-code=${event.code}]`)
      ?.classList.add("active");
    this.#inputGroupEl.classList.toggle(
      "error",
      /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(event.key)
    );
  }
  #onKeyUp(event) {
    if (this.#mouseDown) return;
    this.#keyPress = false;
    this.#keyboardEl
      .querySelector(`[data-code=${event.code}]`)
      ?.classList.remove("active");
  }

  #onChageTheme(event) {
    document.documentElement.setAttribute(
      "theme",
      event.target.checked ? "dark-mode" : ""
    );
  }

  #onChageFont(event) {
    document.body.style.fontFamily = event.target.value;
  }
}

따로 공부가 필요한 내용들

강의를 들으며 알고는 있었지만 정확하게 개념이 부족한 부분들은 따로 공부가 필요했다. 아래의 개념들은 이론노트 블로그에 따로 정리 해 두겠다. 필요한 분들 (미래의 나포함) 은 링크를 타고 가서 보면 좋을 것 같다.

  1. bind()
  2. this

에필로그

물론 혼자 공부하고 혼자 개발 해 보는 것도 중요하지만 강의를 듣게 되면 좋은 점 중에 하나는 나보다 더 잘하는 사람들의 코드를 보며 '저렇게 코드를 짜는구나'라는 방향성이 생긴다. 자바스크립트를 클래스로 사용해서 짜본 적이 없는데 새로운 개념도 얻게 되는 이점이 있다. 물론 모르는 개념들은 따로 찾아보고 정리해야 베스트이지만 말이다.

강의듣고 따라치는 것은 쉽지만 사실상 정리하려고 보면 엄두가 나지 않는다. 지금 이 글 정리도 2주나 지나서야 작성을 완료했다.🤣 하지만 글로 정리해 두면 확실히 내것이 되는 것이 있다.

일전에 퍼블리싱을 배울때에도 오프라인에서 강의를 듣고 직접 손으로 손코딩을 해가며 정리를 했던 기억이 있는데 시간이 오래걸려 비효율적인 것 같았지만서도 퍼블리싱 실무를 할때에는 확실히 그때의 손코딩하며 익혔던 개념들을 떠올리면서 작업을 하게 된다. 정리의 힘을 누구보다 잘 깨닫고 느껴 아주 운이 좋다고 생각한다.

profile
개발하며 얻은 인사이트들을 공유합니다.

0개의 댓글