[21/10/06-07] scss auto import by d.ts && Editor Toolbar 개발

우선 글에 앞서 다음과 같이 에디터 툴바 컴포넌트를 개발하였을때의 문제점을 지적해보자

  • 타입이 너무 많아 코드 가독성이 떨어진다 -> 분리하여 관리할 수 있는 좋은 방법이 필요하다.
  • 이 파일의 전역에 선언된 object객체의 네이밍이 명확하지 않다. -> 임시로 작성한 변수명이기는 하나 변경이 필요해보인다.
  • type에 ~~|null 등의 코드는 좋지 않아보인다. 어떤 방법이 없을지 고민해보자.
import React, { useRef, useState } from 'react';
import Icon from '@/components/Icon';
import * as icons from '@/components/Icon/icons';
import styles from '@/containers/EditorPage/styles/editor.module.scss';
const object: ObjectType = {
  제목: [<div>제목 1</div>, <div>소제목 1</div>, <div>소제목 2</div>],
};
type ObjectOptionType = keyof typeof object;

type ObjectType = {
  제목: React.ReactNodeArray;
};

type CurrentType = {
  제목: React.ReactNode;
};
type EditorDropdownStateType = {
  show: boolean;
  key: ObjectOptionType | null;
  x: number | null;
  y: number | null;
};
type EditorDropdownType = {
  dropdown: EditorDropdownStateType;
};
type EditorToolbarButtonType = {
  icon: keyof typeof icons;
  text: string;
  color?: string;
};

type EditorToolbarSelectType = {
  text: ObjectOptionType;
  setDropdown: React.Dispatch<React.SetStateAction<EditorDropdownStateType>>;
  current: React.ReactNode;
};

function Dropdown({ dropdown }: EditorDropdownType) {
  return dropdown.show ? <div>hi</div> : null;
}
/** select. 제목 - 본문 - 색상 - 정렬 */
function EditorToolbarSelect({ text, current, setDropdown }: EditorToolbarSelectType) {
  const ref = useRef<HTMLDivElement>(null);

  const onClickSelect = () => {
    const el = ref.current;

    setDropdown(prev => ({
      ...prev,
      key: text,
      x: el?.offsetLeft || 0,
      y: el?.offsetTop || 0,
      show: !prev.show,
    }));
  };
  return (
    <>
      <div className={styles.editorToolbarSelectWrapper} onClick={onClickSelect} ref={ref}>
        {current}
        <label>{text}</label>
      </div>
    </>
  );
}

/** button. 이미지 - 굵기 - 휘어쓰기 - 밑줄 - 메모지 - 구분선 - 링크 */
function EditorToolbarButton({ icon, text, color }: EditorToolbarButtonType) {
  return (
    <div className={styles.editorToolbarButtonWrapper}>
      <Icon icon={icon}></Icon>
      <label>{text}</label>
    </div>
  );
}

// function EditorToolbarSelect({ icon, text, color }) {}
function EditorToolbar() {
  const [dropdown, setDropdown] = useState<EditorDropdownStateType>({
    show: false,
    key: null,
    x: null,
    y: null,
  });
  // dropdown 이 변하면 -> css의 변수를 변경시키고, dropdown 클래스는 이 변수를 참조하게 하면 된다.
  // https://stackoverflow.com/questions/49402471/how-to-use-javascript-variables-in-css
  const [current, setCurrent] = useState<CurrentType>({ 제목: object.제목[0] });
  return (
    <>
      <div className={styles.editorToolbarWrapper}>
        <EditorToolbarButton icon={'Image'} text="이미지"></EditorToolbarButton>
        <EditorToolbarSelect
          text="제목"
          current={current.제목}
          setDropdown={setDropdown}
        ></EditorToolbarSelect>

        <EditorToolbarButton icon={'Bold'} text="굵기"></EditorToolbarButton>
        <EditorToolbarButton icon={'Italic'} text="휘어쓰기"></EditorToolbarButton>
        <EditorToolbarButton icon={'Underline'} text="밑줄"></EditorToolbarButton>

        <EditorToolbarButton icon={'Memo'} text="메모지"></EditorToolbarButton>
        <EditorToolbarButton icon={'Seperator'} text="구분선"></EditorToolbarButton>
        <EditorToolbarButton icon={'Link'} text="링크"></EditorToolbarButton>

        <Dropdown dropdown={dropdown}></Dropdown>
      </div>
    </>
  );
}

export default EditorToolbar;

scss의 자동완성 기능을 추가해보자.

완전한 타입 세이프티를 유지하기위해, 개발편의성을 높이기 위해 필요하다

auto import 또 vscode의 인텔리 완성 기능을 추가하기 위해선 d.ts 선언 파일을 작성하여 scss파일을 인지할 수 있도록 하여야한다.

파일구조가 다음과 같다면

editor.module.scss는 자동완성이 되지 않는다. (intellisense)

그렇다면 scss 모듈파일을 생성할 때 마다 d.ts 파일을 선언해야하는가 ? 상당히 불편한 작업일 수 있다.

이를 위해 우린 webpack loader를 이용할 것이다.

typings-for-css-modules-loader

typings-for-css-modules-loader, is a drop-in replacement for css-loader (necessary for CSS Modules) to automatically generate the type definitions for your CSS Modules in webpack. This means it can be used with any CSS preprocessor such as SASS.

scss 파일에 대하여 빌드 타임에 다음과 같은 d.ts(type definition 파일)ㅍ 파일을 생성한다.

이 파일이 존재하게되면, vscode는 scss를 자동완성으로 사용자에게 제공할 수 있다.

declare namespace EditorModuleScssNamespace {
  export interface IEditorModuleScss {
    editorDropdown: string;
    editorToolbarButtonWrapper: string;
    editorToolbarSelectWrapper: string;
    editorToolbarWrapper: string;
    editorWrapper: string;
    resultWrapper: string;
    wrapper: string;
  }
}

declare const EditorModuleScssModule: EditorModuleScssNamespace.IEditorModuleScss & {
  /** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
  locals: EditorModuleScssNamespace.IEditorModuleScss;
};

export = EditorModuleScssModule;

적용하기

module: {
    rules: [
      ...
      
      {
        test: /\.s[ac]ss$/i,
        exclude: /\.module\.scss$/,
        use: [
          { loader: 'style-loader' },

          {
            loader: 'css-loader',
            options: {
              modules: {
                exportLocalsConvention: 'camelCaseOnly',
              },
            },
          },
          { loader: 'sass-loader' },
        ],
      },
      {
        test: /\.module\.scss$/i,
        use: [
          { loader: 'style-loader' },
          {
            loader: '@teamsupercell/typings-for-css-modules-loader',
            options: {
              verifyOnly: isProd,
            },
          },
          {
            loader: 'css-loader',
            options: {
              modules: {
                exportLocalsConvention: 'camelCaseOnly',
              },
            },
          },
          { loader: 'sass-loader' },
        ],
      },
    ],
  },
  • typings-for-css-modules-loader가 아닌 @teamsupercell/typings-for-css-modules-loader를 사용하는 이유는 deprecated 되었기 때문

This specific package has since been deprecated, but there are now several maintained forks and alternatives available:

  • TeamSupercell/typings-for-css-modules-loader
  • dropbox/typed-css-modules-webpack-plugin
  • Megaputer/dts-css-modules-loader
    What if you’re not using webpack?

deprecated되고 다른 대체재로 나타난 @teamsupercell/typings-for-css-modules-loader을 사용하였다.

기존의 버전에서는 css-loader를 대체하였으나, 새롭게 등장한 패키지들은 css-loader를 대체하지않는다. 따라서 css-loader를 사용하여 모듈들을 출력하도록 해야 적용이됩니다.

loader는 오른쪽에서 왼쪽으로 진행합니다. 따라서 다음과 같이 이해하면 편합니다.
{
        test: /\.module\.scss$/i,
        use: [
          { loader: 'style-loader' },
          
          
          // module -> d.ts파일로 타입 생성
          {
            loader: '@teamsupercell/typings-for-css-modules-loader',
            options: {
              verifyOnly: isProd,
            },
          },
          
          
          //css-loader를 통해 모듈 생성
          {
          
            loader: 'css-loader',
            options: {
              modules: {
                exportLocalsConvention: 'camelCaseOnly',
              },
            },
          },
          
          
          // sass loader -> css
          { loader: 'sass-loader' },
        ],
      },

이렇게 생성된 파일들 때문에 빌드가 느려질 수 있으므로 다음과 같이 플러그인을 적용해주어야합니다.

// *scss.d.ts 파일들은 빌드 타임에 무시됩니다.
    new webpack.WatchIgnorePlugin({ paths: [/scss\.d\.ts$/] }),

Webpack rebuilds / builds slow
As the loader generates typing files, it is wise to tell webpack to ignore them. The fix is luckily very simple. Webpack ships with a "WatchIgnorePlugin" out of the box. Simply add this to your webpack plugins:

plugins: [
    new webpack.WatchIgnorePlugin([
      /css\.d\.ts$/
    ]),
    ...
  ]

where css is the file extension of your style files. If you use sass you need to put sass here instead. If you use less, stylus or any other style language use their file ending.

결과물

// editor.module.scss 
@import '@/styles/abstracts/variables';
.wrapper {
  width: 100%;
  height: 200vh;
  display: flex;

  & .editor-wrapper {
    width: 50%;
  }
  & .result-wrapper {
    width: 50%;
  }
}

/* editor toolbar style*/
.editor-wrapper .editor-toolbar-wrapper {
  width: 100%;

  overflow-x: scroll;
  display: flex;
  position: relative;

  & .editor-toolbar-button-wrapper {
    min-width: 80px;
    display: flex;
    flex-direction: column;

    justify-content: center;
    align-items: center;
    & label {
      margin-top: 10px;
      font-size: 12px;
    }
  }

  & .editor-toolbar-select-wrapper {
    min-width: 80px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;

    & label {
      margin-top: 10px;
      font-size: 12px;
    }
  }
  & .editor-dropdown {
    position: absolute;
    bottom: -40px;
  }
}

클래스이름이 camelCase가 된 것은 css-loader를 이용하여 모듈을 생성할 때 class -> camelCase로 변경하도록하였다.

작성할 땐 kebab으로 기존의 규칙을 유지하고 사용할땐 camelCase로 접근하도록 하여, js의 객체 접근 규칙을 유지하였다.

Editor Toolbar 개발

배운점

js의 variable을 이용하는 방법

https://stackoverflow.com/questions/49402471/how-to-use-javascript-variables-in-css

또 overflow 속성이 부여된 element 에선 position: absolute를 사용하여 배치 불가능하다. -> (not fully working)

// editorToolbarSelectWrapper는 overflow-x : scroll이다.
// position:relative 
<div className={styles.editorToolbarSelectWrapper} onClick={onClickSelect} ref={ref}>
        {current}
        <label>{text}</label>
  
  // editorDropdown이 position:absolute; 인 경우
        <div className={styles.editorDropdown}>
          <div>hihihi</div>
          <div>hihihi</div>
          <div>hihihi</div>
          <div>hihihi</div>
        </div>
      </div>

잘리게 된다.

Dropdown을 ToolbarSelectWrapper 바깥으로 빼고 적용해야한다.

https://stackoverflow.com/questions/50178239/css-absolute-position-with-x-scrolling

Ref

좋은 웹페이지 즐겨찾기