현 회사에 이직하기로 마음먹은 건 새로운 도전을 할 수 있을 것 같다는 강한 끌림과 프런트엔드만 접하다가 

간단한 웹 백엔드 서버까지 구축하여 도전을 할 수 있다는 점이 가장 크게 와닿았고 회사의 비전이 정말 괜찮다고 느껴져서 이직 후 6개월가량 밤샘 작업을 하면서 회사의 이바지하였다..

 

기존 전임자가 짜놓고 간 일렉트론으로 만든 코드는.. 정말 형편이 없었고 딜레이부터 잔 버그들 그리고 사용하지 못할 정도로 속도가 느렸고.. 부분 부분 도메인지식도 전체적으로 다 틀렸다.. 전임자가 사용 한 부분은 일렉트론 + mysql + 백그라운드 js를 사용하였다. 정말이지.. 리뉴얼하는데 엄청 진을 뺐다..

 

문제가 되었던 부분을 정리하자면

1. cdn 방식이 아닌 폴더 자체 접근 기존 데이터를 알지 못해서 계속 추가적인.. 중복코드 작성

2. 무분별한 store 사용

3. 이벤트 버스를 사용 함으로 자기가 만들어 놓은 로직이 어디에 있는지 몰라서 추가적으로 중복코드 작성

4. 아키텍처가 없고 규칙이 없는 폴더구조

5. 유지보수가 힘든 명령형 프로그래밍 구조

6. 웹 백엔드가 없어서 백엔드 에러로그 확인이 힘듦

7. 비동기처리에 부재로 시점차이 오류가 남발

8. 라이프사이클을 제대로 활용하지 않아서 시점 차이 발생..

 

전임자가.. SI 출신 12년차 백엔드 개발자라고 전달 받아서 들었다.. 그의 코드를 뜯어보고 나는 경악을 했다.. 신입이 짜놓은 듯한 ... 

 

기존 코드들은 대부분 명령형 + 이벤트버스 남발.. if문에 indexOf.. 등등 es7 등 문법은 찾아보기도 힘들었다..

 

기존 전임자 코드의 아키텍처

사실상 컴포넌트에 tabPages 만 사용 하고 안쓰는 컴포넌트가 태반이였다.

그리고 알 수 없는 main 이라는 폴더는 사용도 안하고 있다..

 

리팩토링한 아키텍처 구조는

 

Api 계층을 담당하는 부분을 common으로 재사용이 가능하게 담아두고 서비스 별로 분리해둔 후 재사용성에 비중을 뒀다..

현 회사의 특이점으로는 폐쇄망으로 내부망쪽에 통신을 해서 값을 올리는 부분이 있는데 그부분에 인수인계가 하나도 없었다.. 순서도를 그려서 대략적인 커스텀마이징을 걸치긴 해야하지만 아래와같이 작업을 진행 하였다.

 

 

순서도로 미리 로직을 그려본 후 코드를 만들어가기 시작했다. 모든 로직을 새로 순서도를 그려서 체계적으로 만들기 시작하였고 새로운 로직으로 버그투성이에다가 돌아가지 않는 전임자 코드를 안쓰고 새로 만들었다.

전임자 코드.... 무분별한 이벤트 버스.. 남발 어디서 뭘하는지 전혀 모르겠다..

되도록 props 를 사용하며 전역 스토어는 지향하였다 스토어에 무분별한 사용도 성능 이슈에 영향을 끼치기 때문에 주의 하면서 쓰도록 하였다.

 

기존 코드 리팩토링 작업을 들어간다.

기존 코드

위에 기존 코드

리팩토링한 코드

if (buff.indexOf('\n') === -1) {
      bufStr += buff.toString()
    } else {
      bufStr += buff.toString()

      self.postMessage({params: JSON.parse(bufStr.toString())})
      bufStr = ''
    }
기존 전임자 코드

ES7에서는 .includes() 메서드가 도입되어 배열에 특정 값이 존재하는지 
확인할 때 더 간결하고 직관적인 방법을 사용할 수 있습니다.
indexOf() 메서드와 비교할 때 가독성 면에서 우수하며
indexOf()는 -1을 반환하는 대신 true 또는 false로 존재 여부를 바로 알 수 있다.

if (!buff.includes('\n')) { // indexOf 대신 includes 사용
  bufStr += buff.toString();
} else {
  bufStr += buff.toString();

  self.postMessage({ params: JSON.parse(bufStr.toString()) });
  bufStr = '';
}

 

 

전임자 코드

async / await 사용 비동기 작업을 효율적으로 관리하여 성능을 크게 향상시킬 수 있다.

기존의 콜백 함수나 Promise 체인보다 훨씬 가독성이 좋고, 비동기 코드 작성을 간소화하여 네트워크 요청, 파일 읽기/쓰기 등의 작업을 최적화할 수 있다.

대략 위에 같이 함수형 프로그래밍으로 변경을 했고

성능쪽 이슈를 해결해보겠다.

 

기존 테이블 리스트 5개 기준 속도 측정 결과 

기존 전임자 일렉트론 코드

 

리팩토링 코드

레디스 캐시 서버와 프론트 측 불필요한 스크립트를 걷어내서 속도 향상을 하였고 불필요한 비동기 함수들을 걷어냈다.

 

서버측에서는 TCP WEBSOCKET  을 사용하여 통신 측 서비스를 하나 만들었고 거기에 로거 서비스를 만들어서 로그를 생성해서 폴더에 생성했다. 아래는 서비스 로거 코드

private lastMessages = {
    log: null,
    error: null,
    warn: null,
    debug: null,
    cbcLis: null,
    ping: null,
    login: null,
  }; // 각 로그 레벨별 마지막 메시지 저장

  constructor() {
    super();
    this.ensureBaseLogDirectoryExists();
  }

  log(message: string) {
    if (this.isDuplicateMessage('log', message)) return;
    super.log(message);
    this.writeLog('log', message);
  }

  error(message: string, trace?: string) {
    const fullMessage = trace ? `${message}\n${trace}` : message;
    if (this.isDuplicateMessage('error', fullMessage)) return;
    super.error(message, trace);
    this.writeLog('error', fullMessage);
  }

  warn(message: string) {
    if (this.isDuplicateMessage('warn', message)) return;
    super.warn(message);
    this.writeLog('warn', message);
  }

  debug(message: string) {
    if (this.isDuplicateMessage('debug', message)) return;
    super.debug(message);
    this.writeLog('debug', message);
  }

  cbcLis(message: string) {
    if (this.isDuplicateMessage('cbcLis', message)) return;
    super.log(message);
    this.writeLog('cbcLis', message);
  }

  ping(message: string) {
    if (this.isDuplicateMessage('ping', message)) return;
    super.log(message);
    this.writeLog('ping', message);
  }

  logic(message: string) {
    if (this.isDuplicateMessage('logic', message)) return;
    super.log(message);
    this.writeLog('logic', message);
  }

  private isDuplicateMessage(level: string, message: string): boolean {
    if (this.lastMessages[level] === message) {
      return true; // 중복 메시지일 경우 true 반환
    }
    this.lastMessages[level] = message; // 마지막 메시지를 갱신
    return false; // 중복이 아니면 false 반환
  }

  private formattedTime(date: Date) {
    const hours = String(date.getHours()).padStart(2, '0');
    const minutes = String(date.getMinutes()).padStart(2, '0');
    const seconds = String(date.getSeconds()).padStart(2, '0');
    const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
    return `[${hours}:${minutes}:${seconds}.${milliseconds}]`;
  }

  private writeLog(level: string, message: string) {
    const now = new Date();
    const dateString = moment(now).format('YYYY-MM-DD'); // 현재 로컬 시간 기준으로 날짜 생성
    const logDir = path.join(this.baseLogDir, level);

    this.ensureDirectoryExists(logDir);

    const logFilePath = path.join(logDir, `${dateString}_${level}.txt`);

    this.ensureFileExists(logFilePath);

    const formattedMessage = `${this.formattedTime(now)} - ${message}`;
    fs.appendFileSync(logFilePath, `${formattedMessage}\n`);
  }

  private ensureBaseLogDirectoryExists() {
    this.ensureDirectoryExists(this.baseLogDir);
  }

  private ensureDirectoryExists(dir: string) {
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
      console.log(`로그 디렉토리가 존재하지 않아서 생성: ${dir}`);
    }
  }

  private ensureFileExists(file: string) {
    if (!fs.existsSync(file)) {
      fs.writeFileSync(file, '');
    }
  }

 

아래는 생성된 로그 폴더

로컬 PC에 맞게 로그도 남기면서 성능 이슈도 해결 했다.

서버 측 작업과 프론트 쪽에서도 많은 작업을 했지만 나중에 추후에 더 깊게 작성 해볼 예정이다! 끝

이직 한 회사에서 Ai 코어 측에서 만들어주는 이미지 폴더안에 

이런식으로 폴더가 생성되어있는곳에서 이미지를 조합해서 화면에 표시해줘야하는 작업이 생겼다.

json으로는 좌표를 표시해서 화면에서 해당 좌표에 사각형으로 표시를 해주는 작업이 추가 되었다.

이제 로직을 구현 해볼것이다.

마운티드에 함수 실행 할 부분을 추가 해준다.

const fetchTilesInfo = async (folderPath: string) => {
  const url = `${apiBaseUrl}/folders?folderPath=${folderPath}`;
  const response = await fetch(url);

  if (!response.ok) {
    tileExist.value = false;
    throw new Error('Network response was not ok');
  } else {
    const fileNames = await response.json();
    const tilesInfo = [];
    fileNameResultArr.value = [];
    for (const fileName of fileNames) {
      if (fileName.endsWith('_files')) {

        const fileNameResult = extractSubStringBeforeFiles(fileName);
        fileNameResultArr.value.push(fileNameResult)
        const {width, height} = await dziWidthHeight(fileNameResult)

        tilesInfo.push({
          Image: {
            xmlns: "http://schemas.microsoft.com/deepzoom/2009",
            Url: `${apiBaseUrl}/folders?folderPath=${folderPath}/${fileName}/`,
            Format: "jpg",
            Overlap: "1",
            TileSize: "1024",
            Size: {
              Width: width,
              Height: height
            }
          }
        });

        canvasCurrentWitdh.value = width;
        canvasCurrentHeight.value = height;
      }
    }
    tileExist.value = true;
    return tilesInfo;
  }
};

일단.. 웹 회사에서 근무할 때 선언형(함수형 프로그래밍)을 많이 써왔지만 사이드 이펙트가 발생 할 여지가 있지만  파일 폴더 측에 접근해서 좀 더 직관적인 명령형으로 어떤식으로 흘러가는지 파악하기 위해서 명령형으로 작성해준다. 

 

먼저 백엔드 측에서 이미지를 CDN 방식으로 만들어둔 Api 를 통해서 응답을 받아온다.

응답 값

제대로 담기는 모습을 확인 후 

viewer.value = OpenSeadragon({
        id: "tiling-viewer_img_list",
        animationTime: 0.4,
        navigatorSizeRatio: 0.25,
        showNavigator: true,
        sequenceMode: true,
        defaultZoomLevel: 1,
        prefixUrl: `${apiBaseUrl}/folders?folderPath=D:/UIMD_Data/Res/uimdFe/images/`,
        tileSources: tilesInfo,
        showReferenceStrip: false,
        gestureSettingsMouse: {clickToZoom: false},
        maxZoomLevel: 15,
        minZoomLevel: 1, // 최소 확대 레벨 설정
        zoomPerClick: 1.2, // 클릭 확대 비율 설정
        zoomPerScroll: 1.2, // 스크롤 확대 비율 설정
        viewportMargins: {top: 0, left: 0, bottom: 0, right: 0}, // 뷰포트 여백 설정
        visibilityRatio: 1.0 // 이미지를 뷰포트에 맞추기 위한 비율 설정
      });

위에처럼 작성 후 담아주고 

// 마그니파이어 설정 - 동그라미 줌기능
      new OpenSeadragon.MouseTracker({
        element: viewer.value.element,
        moveHandler: function (event) {
          const existingMagCanvas = document.getElementById('magCanvas');
          if (existingMagCanvas) {
            viewer.value.element.removeChild(existingMagCanvas);
          }

          if (!isMagnifyingGlass.value) {
            return;
          }

          const {canvas} = viewer.value.drawer;
          const magCanvas = document.createElement('canvas');
          const magCtx = magCanvas.getContext('2d');
          canvasOverlay.value = magCanvas;
          if (magCtx) {
            const magWidth = 200;
            const magHeight = 200;
            const zoomLevel = 5;

            magCanvas.id = 'magCanvas';
            magCanvas.width = magWidth;
            magCanvas.height = magHeight;
            magCanvas.style.position = 'absolute';
            magCanvas.style.left = `${event.position.x - magWidth / 2}px`;
            magCanvas.style.top = `${event.position.y - magHeight / 2}px`;
            magCanvas.style.border = '1px solid';
            magCanvas.style.borderRadius = '50%';
            magCanvas.style.width = `${magWidth}px`;
            magCanvas.style.height = `${magHeight}px`;
            magCanvas.style.zIndex = '0';

            viewer.value.element.appendChild(magCanvas);

            // 줌을 위한 확대된 부분을 정확히 잘라내기 위해 drawImage 메서드 수정
            magCtx.drawImage(
                canvas,
                event.position.x - (magWidth / 2 / zoomLevel),
                event.position.y - (magHeight / 2 / zoomLevel),
                magWidth / zoomLevel,
                magHeight / zoomLevel,
                0,
                0,
                magWidth,
                magHeight
            );

            magCanvas.style.visibility = event.position.y <= 0 || event.position.x <= 0 ? 'hidden' : 'visible';
          }
        },
      });
      
      viewer.value.addHandler('open', function (event: any) {
        // 캔버스 크기를 조정
        canvas.width = event.source.Image.Size.Width;
        canvas.height = event.source.Image.Size.Height;
      });

      viewer.value.addHandler('page', function (event: any) {
        const notCanvasClick = fileNameResultArr.value[event.page] !== 'RBC_Image_0';
        emits('notCanvasClick', notCanvasClick);
        // 페이지가 변경될 때 오버레이를 다시 추가
        if (canvas.parentElement !== viewer.value.container) {
          viewer.value.addOverlay({
            element: canvas,
            location: new OpenSeadragon.Rect(0, 0, 1, 1),
          });
        }
        emits('unChecked');
      });
     viewer.value.addHandler('zoom', () => {
        if (activeRuler.value === 'None') {
          return;
        }
        drawRuler(activeRuler.value);
      });
      
      viewer.value.addHandler('canvas-click', async (event: any) => {
      if (!event.originalEvent.ctrlKey) {
          await removeDiv();
        }
        
         const clickPos = viewer.value.viewport.pointFromPixel(event.position);
          const canvasPos = {
            x: clickPos.x * viewer.value.source.width,
            y: clickPos.y * viewer.value.source.height
          };

          // 함수: 클릭 위치가 아이템 위치와 겹치는지 확인
          const isItemSelected = (item: any) => {
            const width = item.width;
            const height = item.height;
            return (
                canvasPos.x >= Number(item.posX) && canvasPos.x <= (Number(item.posX) + width) &&
                canvasPos.y >= Number(item.posY) && canvasPos.y <= (Number(item.posY) + height)
            );
          };

          // 선택된 아이템의 classNm 저장
          let selectItm = '';
          for (const item of drawPath.value) {
            if (item.classNm !== "Normal" && isItemSelected(item)) {
              selectItm = item.classNm;
              break; // 하나의 아이템만 선택됨
            }
          }
          아래 코드는 생략

줌 기능, OPEN , 페이지넘기기, 캔버스 클릭 시 사각형 생성 등등.. 필요한 부분들을 넣어준다.

 

혈액 RBC에는 클래스 리스트에 맞게 체크를 넣어줘야 하므로 watch 로 현재 클릭 되어있는 체크배열을 받아서 그려주는 로직을 추가 해준다. any 가 많은건.. 혼자서 전체 로직을 짜고 데모 버전으로 ㅠ 3개월만에 완성을 시켜야해서 다급하게 하는라 타입을 지정해주지 못했다 ㅠㅠㅠㅠㅠㅠ 따로 추후에 리팩토링 과정을 통해서 전체 수정할 예정이다..!

 

watch(() => props.classInfoArr, (newData) => {
  newItemClassInfoArr.value = newData;

  if (newData.length === 0) {
    removeDiv();
    removeRbcMarker();
    return;
  }

  // 모든 <ol> 요소를 선택하고, data-class-nm 값을 배열로 수집
  const olElements = document.querySelectorAll('ol.overlayElement');

  // newData 배열에서 존재하는 data-class-nm 값을 수집
  const validClassNmSet = new Set(newData.map((el: any) => el.classNm));

  olElements.forEach(el => {
    const classNm = el.getAttribute('data-class-nm');

    // data-class-nm이 newData에 존재하지 않으면 해당 <ol> 요소를 제거
    if (!validClassNmSet.has(classNm)) {
      console.log('Removing <ol> with data-class-nm:', classNm);
      viewer.value.removeOverlay(el);
    }
  });

  // rbcMarker 함수 호출
  rbcMarker(newData);
}, {deep: true});

대략적인 모든 기능을 구현이 완성 된 후 화면은 아래와 같다

 

혼자서...3일안에 완성시킨.. 오픈시드래곤을 활용한 타일화 ! 좀 고생을 많이 했다 오픈시드래곤 공식 문서를 보면서 일일이 다 고치는라 고생한 ... 정말 힘들었다. 추후에 타입추가와 리팩토링을 걸쳐서 수정해야겠다.

Vue 3에서 소개된 Composition API는 컴포넌트 로직을 보다 간결하게 작성하고 재사용하기 위한 강력한 도구다. 이 글에서는 실제로 사용된 코드를 통해 Composition API의 핵심 원리를 살펴보겠다.

핵심 개념
1. ref 함수

import { ref } from 'vue';

const myVar = ref(0);

ref 함수는 반응적인(reactive) 데이터를 생성한다. 이렇게 생성된 변수는 변경되면 자동으로 화면을 업데이트한다. myVar.value로 현재 값을 얻을 수 있다.

 

 

 

2. reactive 함수, computed 함수, watch 함

import { reactive } from 'vue';

const myObject = reactive({ prop: 'value' });

import { computed } from 'vue';

const double = computed(() => myVar.value * 2);


import { watch } from 'vue';

watch(() => myVar.value, (newValue, oldValue) => {
  console.log(`myVar changed from ${oldValue} to ${newValue}`);
});

 

reactive 함수는 객체를 반응적으로 만들어준다. 객체의 속성이 변경되면 화면이 자동으로 갱신된다. 
computed 함수는 계산된 값을 만들어준다. 종속성을 추적하여 해당 값이 변경될 때까지 다시 계산하지 않는다.

watch 함수는 데이터의 변화를 감지하고 그에 따른 동작을 수행할 수 있도록 한다.

옵션 api 에서는 구조적으로 있었으나 import 선언으로 쓸 수 있게 변경이 되었다.

3. 비동기 작업과 onMounted

const adminCategoriesListInit = async () => {
  loadCategories();
};

onMounted(adminCategoriesListInit);

 

onMounted는 컴포넌트가 마운트된 후 실행되는 라이프사이클 훅입니다. 비동기 작업을 수행하는 함수를 호출하여 초기 데이터를 가져올 수 있다. 모든 라이프 사이클 에는 on 이 붙는다.

 

4. setup 함수

setup() {
  // 초기 설정 및 로직 작성
  return { myVar, double };
}

 

setup 함수는 컴포넌트의 초기 설정을 담당하며, 여기서 리액티브 데이터, 메소드, 훅 등을 반환한다.

 

 

Vue 3 Composition API는 코드를 보다 모듈화하고 가독성 있게 작성할 수 있는 강력한 기능을 제공한다. 이를 통해 개발자는 더 효율적으로 코드를 작성하고 유지보수할 수 있다. 기존 옵션 api 는 다르게 구조는 파악이 힘들지만 더 간결한 코드로 작성이 가능하다.

 

 

Vue 자바스크립트 기반의 프레임워크

Vue.js(뷰 제이에스)는 자바스크립트 기반의 프론트엔드 프레임워크로, 사용자 인터페이스를 구축하기 위한 라이브러리입니다. 다른 프레임워크와 비교했을 때 가볍고 진입 장벽이 낮으며, 쉬운 문법과 뛰어난 성능으로 개발자들 사이에서 인기를 얻고 있다.

 

주요 특징:

  1. 바인딩 (Data Binding):
    • Vue.js는 데이터와 화면 요소 간의 양방향 데이터 바인딩을 지원합니다. 이는 모델과 뷰 간의 동기화를 쉽게 유지할 수 있도록 도와준다
  2. 컴포넌트 기반 구조:
    • Vue.js는 컴포넌트 기반 아키텍처를 채택하고 있습니다. 이는 애플리케이션을 작은 단위로 나누어 각각을 재사용 가능한 컴포넌트로 구성함으로써 유지보수성을 높이고 코드의 가독성을 향상시긴다.
  3. 가상 돔 (Virtual DOM):
    • Vue.js는 가상 돔을 활용하여 실제 DOM 조작을 최소화하고 성능을 향상시킵니다. 변경된 부분만을 실제 DOM에 적용하여 불필요한 렌더링을 방지한다.
  4. 디렉티브 (Directives):
    • Vue 디렉티브는 v- 접두어를 가진 특수 속성으로, HTML에서 사용자 정의 속성처럼 사용됩니다. 예를 들어, v-if, v-for, v-bind, v-on 등이 있다.
  5. 이벤트 핸들링 및 메소드:
    • Vue에서는 v-on 디렉티브를 통해 이벤트 핸들링을 지원하며, 메소드를 정의하여 이벤트에 대한 로직을 처리할 수 있다.
  6. 라우팅 (Vue Router):
    • Vue Router를 이용하여 SPA(Single Page Application)를 쉽게 구현할 수 있습니다. 라우터를 사용하면 페이지 간의 전환과 브라우저 히스토리를 관리할 수 있다.
  7. 상태 관리 (Vuex):
    • 대규모 애플리케이션에서 상태 관리를 위한 Vuex를 제공합니다. 중앙 집중식 상태 관리 패턴을 사용하여 애플리케이션의 상태를 효과적으로 관리한다.
  8. 라이프사이클 훅:
    • Vue 인스턴스의 라이프사이클에 특정 로직을 실행할 수 있는 훅을 제공하여 애플리케이션의 특정 시점에 작업을 수행할 수 있다.

최근 회사에서는 react 로 작업중에서 느낀점이지만 vue 는 좀더 가독성이 높고 간결한 코드를 작성하기에 최적화 되어있다고 생각이 들었다. 프레임워크로 리액트는 라이브러리를 추가하여 리덕스 zustand 를 사용하여 상태관리를 하는 방면 vue 는 따로 store 관리를 라이브러리 설치를 하지 않고도 진행 할 수 있는 점이 가장 큰 장점으로 다가왔다.

 

라이프 사이클또한 

공식문서 캡처 내용

위에처럼 여러가지로 생명주기 훅으로 사용한다. 리액트에서는 useEffect 로 동기화 작업을 한다면 vue 는 라이프 사이클로 전반적인 동기화 작업을 진행시킨다. 의존성을 주입해서 감시하는 useEffect 와 차이점으로는 감시자는 watch 라는 감시자가 있는데 리액트와 다른 강점을 가지고 있다. 하지만 watch 는 주의해야할점이 있다. 명령형 프로그래밍 방식이므로

computed 속성을 사용해야하는경우는 구분하여 사용해야한다. 선언형 프로그래밍 방식인 컴퓨티트는 computed 속성은 종속 대상을 따라 저장하며 캐싱처리를 하기떄문에 구분하여 사용 한다.

 

vue3

vue3가 도입되면서 달라진 점이 있다.

2022년 1월에 새로 도입된 vue3는

1. 성능 향상

 

[가상 DOM 최적화]

 

기존 Vue 렌더링의 가상 DOM 설계 HTML 기반 템플릿을 제공하고 이를 가상 DOM Tree로 반환한 후 실제 DOM의 어떤 영역이 업데이트되어야 하는지 재귀적으로 탐색하는 방식이었다. 그렇기에, 매 변경을 파악하기 위해 모든 트리를 확인하는 비효율성이 존재했다.

  • 템플릿 구문에서 정적 요소와 동적 요소를 구분, 트리를 순환할 떄 동적 요소만 순환해서 탐색의 최적화를 반영했다.
  • 렌더링 관련 객체(템플릿 내 정적 요소, 서브 트리, 데이터 객체) 등을 컴파일러가 탐지해 Renderer 함수 밖으로 호이스팅시켜 객체의 복수 생성을 방지한다.
  • 컴파일러가 템플릿 내 동적 바인딩 요소에 플래그를 생성한다. 이를 통해 렌더링 속도를 향상시켰다.

 

[트리쉐이킹 강화]

 

트리 쉐이킹(Tree Shaking)이란 나무를 흔들어 잎을 떨어트리듯 모듈을 번들링하는 과정에서 사용하지 않는 코드를 제거하여 사이즈를 줄이는 최적화 방안을 의미한다.

Vue3는 컴파일러가 실제 사용하는 코드만 import하며, v-model과 같은 양방향 바인딩에서 트리 쉐이킹을 적용하여 번들 크기를 절반 이상으로 줄인다.

 

2. Composition API

Vue3의 가장 큰 특징이라고 할 수 있는 Composition API가 등장함에 따라, 함수형 프로그래밍 기반의 코드 템플릿의 변화가 일어났다.

여기서는 대표적으로 바뀐 부분들만 짚고, 자세한 내용은 별도로 정리한 포스팅을 참고해주기를 바란다!

  • setup() 메서드 : 기존 컴포넌트 옵션들을 setup() 메서드 내에 선언 및 반환한다. 데이터에 반응형을 부여하는 ref() 및 reactive(), 기존의 computed(), watch() 모두 API 메서드들로 대체되었다.
  • props : this 바인딩을 하던 방식에서, setup() 의 첫번째 인자로 받아 내부에서 활용할 수 있다.
  • emit : this 바인딩을 하던 방식에서, setup() 의 두번째 인자인 context에 포함되어있다.
  • Lifecycle Hooks : beforeCreate, created 가 setup() 으로 대체된다. 또, hooks 앞에 on들이 붙었으며, destroy는 unmount로 변경되었다.

이러한 컴포지션 API를 통해 코드의 가독성 향상, 재사용성의 개선, Typescript 타입 추론 등의 지원이 가능해졌다.

 

3. Fragment

Vue2 에서는 <template> 내에 단일 태그로 랩핑을 필수적으로 해야 했다. (Vue 인스턴스를 단일 DOM 요소로 바인딩했어야함)

하지만, Vue3 는 <Fragment> 태그를 지원하며, 이를 통해 다중루트 노드(multiple root node)를 작성할 수 있게 되었다. (Fragment는 DOM 트리에 그려지지 않음)

 

<!-- Vue2 -->
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>


<!-- Vue3 -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

 

4. Teleport

React의 Portals(포털)과 유사한 기능으로, 모달이나 알림 등과 같이 특정 컴포넌트가 부모에 상속되어 있으면서도 렌더링되는 위치는 제 2의 루트(영역)인게 유리한 경우 Teleport 기능이 유용하다.

이를 통해, 기존에 CSS로 조정하거나 인위적으로 엘리먼트를 주입하던 방식에서, <teleport> 태그 내부의 HTML 요소를 특정 태그로 옮겨 렌더링할 수 있게 되었다.

 

// Modal.vue
<teleport to="#deleteModal">
  <Modal v-show="showModal" @delete="deleteTodo" @close="closeModal"></Modal>
</teleport>

 

위 예시와 같이, 다른 곳에 렌더링할 요소를 <teleport> 태그로 감싸고, to 속성에 렌더링할 위치의 id(혹은 class)명을 설정한다.

 

// index.html
<body>
  <div id="app"></div>
  <div id="deleteModal"></div>
</body>

 

다음과 같이, Teleport의 타겟이었던 #deleteModal 내에 엘리먼트가 렌더링되는 걸 볼 수 있다.

 

확실히 Composition API가 Vue3의 가장 큰 변경점이다. 이외엔, Teleport, Suspense 와 같이 부가기능 컴포넌트들이 주를 이루었다.

다만, 가상DOM 트리를 개선한 부분이 기존 Vue2는 어땠고, Vue3는 어떻게 개선되었는지 한번쯤 짚고 넘어갈 것을 권장한다.

오늘 회사에서 관리자 쪽에서 특정 매장을 운영상태를 끄면 사이트에서 계속 노출이 된다는 요청사항이 들어와서 소스코드를 분석했다.

기존 코드에서 fetch 에서 백엔드 api 호출 후 데이터를 받는 부분이 있엇다. 새로고침에서는 mounted 에서 제대로 소스코드가 작동을 했지만 뒤로가기 버튼을 통한 매장접속시 백엔드 api 가 작동을 안함을 확인 할 수 있었다. 

 

fetch는 비동기 데이터 호출을 위한 Hook 이며 공식문서에서 간략한 설명으로는

  • fetch hook (Nuxt 2.12+)은 모든 컴포넌트에서 사용할 수 있고, (client-side 렌더링 중) 렌더링이 진행중인 상태와 에러에 대한 참조(shortcuts)를 제공합니다.

페치는 컴포넌트 인스턴스가 생성된 후 server-side 렌더링 동안 또는 client-side 에서 네비게이션 되는 동안 호출되는 hook으로, 비동기 데이터 호출이 완료되었을때 프로미스를 반환한다. 페치는 불필요한 리렌더링을 방지 하기위하여 캐시를 저장하여 데이터를 보존한다. 

mounted 라이프 사이클 쪽에서 페치에서 백엔드 api 쪽에서 전달해준값을 재렌더링으로 사용하지 못하고있엇다.

서버사이드쪽에서 로그가 찍히므로 테스트 진행시에는 로컬서버에서 콘솔로 찍히는값으로 확인이 필요하다.

위와같이 쓰면 터미널 쪽에서는 아래와같이 데이터가 찍힌다.

사이트네임을 넣어두지 않아서 널값이다. 정상적으로 작동을하는것을 확인하였고. 뒤로가기 했을시에는 로그가 안찍히는것을 확인하였다.

mounted 에서만 호출을 하면 재렌더링 되는 부분에서 함수동작을 못한다. 이럴경우 나는 updated 에서 감지하기로 하였다.

 

updated 부분에서 스토어에서 저장한값을 체크한 후 매장이 정상적인 플래그값을 가지고 있는지 재차 확인하는 소스코드를 작성하였다.

checkIsStoreUsed(storeInfo: any): void {

    // siteStore.updateExcludedChannels(false);
    let excludedChannel = false;
    let excludedChannelStr = '';

    for (const i in storeInfo?.excludedChannels) {
      if (String(storeInfo?.excludedChannels[i]) === 'WWW') {
        excludedChannelStr = 'www';
      }
    }
    siteStore.setSoreInfo(this.storeInfo);
    sessionStorage.setItem('storeInfo', JSON.stringify(this.storeInfo));
    if (storeInfo?.enabled === false || excludedChannelStr) {
      excludedChannel = true;
    } else if(storeInfo?.enabled === true && excludedChannelStr === ''){
      excludedChannel = false;
    }

    this.checkIsStoreUsedMessage(excludedChannel);


  }

  checkIsStoreUsedMessage(excludedChannel: boolean): void{
    if (excludedChannel) {
      // this.excludedChannels = true;
      siteStore.updateExcludedChannels(true);
      alert('잘못된 접근입니다.');
      if (!BizModeDefines.saas) {
        this.$router.replace('/intro');
      }else{
        this.$router.replace('/');
      }

    }
  }
//
  // 매장 정보
  //
  private _storeInfo: StoreInfoResponseDto | null = null;

  get soreInfo(): StoreInfoResponseDto | null {
    return this._storeInfo;
  }

  @Mutation
  public setSoreInfo(soreInfo: StoreInfoResponseDto | null): void {
    this._storeInfo = soreInfo;
  }

  @Action
  public updateSoreInfo(soreInfo: StoreInfoResponseDto | null): void {
    this.context.commit('setSoreInfo', soreInfo);
  }

  //
  // 매장 채널 리스트
  //
  private _excludedChannels: boolean = false;

  get excludedChannels(): boolean {
    return this._excludedChannels;
  }

  @Mutation
  public setExcludedChannels(excludedChannels: boolean): void {
    this._excludedChannels = excludedChannels;
  }

  @Action
  public updateExcludedChannels(excludedChannels: boolean): void {
    this.context.commit('setExcludedChannels', excludedChannels);
  }

일단 스토어쪽에 매장 정보를 저장할 수 있는 공간을 만들고 다른페이지에서도 매장을 감지하는 부분이 필요해서 추가적으로 작성해주었다.

 

this.$router.replace 를 사용한이유는 push 를 이용하면 히스토리가 남으면서 추가적으로 다른 이슈상황들이 생기는 부분을 미리 방지하는 차원에서 this.$router.replace 를 사용하면 페이지 이동한다고 보기에는 현재 페이지를 바꿔준다는 개념으로 사용하여 다른 추가적인 에러가 날 수 있는 부분을 방지할수있다.

 

 

 

위와 같은 얼럿 표시 이 후 intro 페이지로 이동한후

 

정상적으로 페이지 이동도 했다.!

다시 뒤로가기 버튼으로 백해도 스토어쪽으로 다시 재진입을 하지못한다!

이걸로 페치를 이용하여 정보를 스토에 담아두고 mounted에서 호출 하는게 아니라 updated에서 재렌더링을 감지하여 코드를 넣어주면 특정 코드를 다시 실행 시킬 수 있게 되었다.

 

이걸로 채널 제외 매장을 화면에서 막을 수 있게 되었다.! 코인의 일지 끄읏~

nuxt 를 사용하여 개발이아닌 운영서버에 배포시 가끔 이런에러가 목격이 된다..

Product 환경에 배포하려고 할 때 발생되는 현상으로,

번들링에서 생기는 오류로써 이를 해결하기 위해서는

gulp 같은 방식을 사용해야 하는데 따로 설치를 하지 않고도 처리를 진행할 수 있는 방법이

nuxt에서는 가능하다.

 

nuxt.config.js 에 들어가서

요부분을 추가해주고 다시 배포하면 해당에러는 해결이 되었다~

 

정상 동작을 확인하였다~

+ Recent posts