자바스크립트에서 event를 컨트롤 할 때 preventDefault라는 메서드를 자주 접하게 된다.

 

event.preventDefault()는 해당 event가  cancelable 할때 (cancelable이 true일 때) 해당 이벤트의 기본 동작의 수행을 막는 메서드이다. 모든 event가 cancelable 하지는 않으니 모든 이벤트에 유효한 메서드는 아니다. cancelable 하지 않은 이벤트는 preventDefault를 호출해도 아무 반응이 일어나지 않는다.

 

 

예를 들어서 체크박스의 클릭이벤트를 막아 체크박스를 체크하지 못하게 만든다거나, submit 이벤트에 사용하여 데이터를 전송하지 못하게 하는 등 다양한 방면에서 사용 가능하다.

 

그러나 preventDefault는 이벤트 전파를 막지는 못합니다. 상위요소로의 이벤트 전파를 막기 위해서는 stopPropagation() 또는 stopImmediatePropagation()을 함께 사용해야합니다.

 

참고

https://developer.mozilla.org/ko/docs/Web/API/Event/preventDefault

 

Event.preventDefault() - Web API | MDN

Event 인터페이스의 preventDefault() 메서드는 어떤 이벤트를 명시적으로 처리하지 않은 경우, 해당 이벤트에 대한 사용자 에이전트의 기본 동작을 실행하지 않도록 지정합니다.

developer.mozilla.org

 

1.map

각 요소에 대해 주어진 함수를 수행하는 결과를 모아 새로운 배열을 반환한다.

ex) let newArray = testArray.map((value, index, array) => {

           // value : 현재 요소의 값, index : 현재 요소의 인덱스, array : 원본 배열
            return value * 2;
       });

 

2.filter

각 요소에 대해 주어진 함수의 결과값이 true인 결과를 모아 새로운 배열을 반환한다.

ex) let newArray = testArray.filter((value, index, array) => {

           // value : 현재 요소의 값, index : 현재 요소의 인덱스, array : 원본 배열
            return value * 2 > 0 //boolean 값 이어야 한다.
       });

 

map과는 다르게 return 값이 boolean 이고, true인 결과를 모아 새로운 배열을 반환하는데, 메서드 이름 그대로 , 어떤 배열을 조건에 맞게 필터링할 때 (ex validation을 체크해서 새로운 배열을 만들 때) 사용하기 쉽다. 

 

3.reduce

배열 각 요소에 대해 reducer함수를 실행하고 배열이 아닌 하나의 결과값을 반환한다.

ex) const numberList = [1, 2, 3, 4];
 const sum = numberList.reduce((acc, value) => { 
        //acc : 축적된 데이터 value : 현재 요소의 값
        return acc + cur; 
    }, 10 // 초기 값);
예를들어 위와 같이 사용하였을 때는 10 + 1 + 2 + 3+ 4 의 계산결과인 20이 sum에 할당 되게 된다.
초기값에 빈 배열을 할당하고 value를 그 배열에 push 하는 형태로 사용하면 새로운 배열을 만들어 낼 수 있다는 점에서 filter와 map을 다 아우를 수 있는 메서드이다. 다양한 방면에 적용이 가능하고 퍼포먼스 적인 측면에서도 가장 낫기 때문에 가장 많이 쓰인다. 

 

 
 

자바스크립트 런타임 구성요소

setTimeout 의 콜백함수가 delay시간 이후에 정확하게 호출되지 않는 이유는 자바스크립트 런타임 구성요소를 알아야 이해가 쉽다.

 

자바스크립트는 기본적으로 싱글 스레드 이므로 한개의 작업만을 다룰 수 있다. 그렇기 때문에 하나의 프로그램에서 동시에 하나의 코드만 실행할수있다. 

 

1) Call Stack

 자바스크립트 메서드 호출 시 실행 순서대로 call stack에 쌓이게되고(push), 함수 실행이 완료되면 스택에서 제거된다.(pop)(선입선출)

setTimeout도 호출되면 callStack에 먼저 쌓이게된다. 이후 호출은 완료되었기 때문에 stack에서 지워지게된다. 

 

2) Callback Queue

 

WebAPI의 콜백이 쌓이는 곳이다. 설정했던 delay시간이 지나고 나면 setTimeout의 콜백이 쌓이게 된다.

Web API는 자바스크립트가 실행되는 런타임 환경에 존재하는 별도의 API 이다. (V8 소스코드에는 존재하지 않는다.) ex) setTimeout, setInterval, setImmediate 등..

 

3) Event Loop

이벤트 루프는 콜 스택이 빌 때까지 기다린 후 콜백 큐에 있는 콜백을 콜 스택에 넣어주는 역할을 한다. 

 

다시 한번 설명하면 1. setTimeout이 실행된 시점에 Call Stack에 setTimeout이 쌓이게 되고, 호출이 완료된 후 제거된다.

2. 호출 이후 지정한 delay시간이 지나고나면 setTimeout의 콜백함수가 Callback Queue에 쌓이게 된다.

3. 이벤트 루프는 콜스택이 비게되면 콜백 큐에 있는 콜백함수를 Call Stack에 넣어준다.

 

이때 Call Stack에 다른 할일이 많이 쌓여있다면 콜백함수가 호출되는 시점은 더 늦어질 것이다. 그렇기 때문에 setTimeout으로 지정한 delay시간이 지나고 나서 지연시간이 생기게 된다. 

 

 

setTimeout 이란?

일정 시간이 지나고 지정한 코드를 실행시키는 메서드로 보통 아래와 같은 형태로 자주 사용한다.

setTimeout(() => {console.log("첫 번째 메시지")}, 5000);
setTimeout(() => {console.log("두 번째 메시지")}, 3000);
setTimeout(() => {console.log("세 번째 메시지")}, 1000);

위에서 , 뒤에 있는 숫자가 지연 시간을 나타내는데, 단위는 밀리세컨드이다. (1000 = 1초)

 

정확히 지정한 시간 이후 코드가 실행될까?

그런데 실제로 사용해보면 정확하게 지정한 시간이 지났을때 코드가 실행되지 않고 지연되는 경우가 발생한다. 글쓴이의 경우 스크롤을 이동시킬 때 setTimeout에 딜레이를 0으로 주고 그 안에서 scrollTo메서드를 사용할 때와 setTimeout으로 감싸지 않고 scrollTo를 사용할 때 결과에 차이가 발생하여(setTimeout으로 감싼 경우에만 스크롤이 이동하였다) setTimeout에대해 더 찾아보게 되었고, 그 결과 다음과 같은 문서를 보게 되었다. 실제로 찾아보면 scroll이동 이벤트는 많은 사람들이 setTimeout과 함께 사용하는 것을 추천하고 있다.

 

https://developer.mozilla.org/ko/docs/Web/API/setTimeout#%EB%94%9C%EB%A0%88%EC%9D%B4%EA%B0%80_%EC%A7%80%EC%A0%95%ED%95%9C_%EA%B0%92%EB%B3%B4%EB%8B%A4_%EB%8D%94_%EA%B8%B4_%EC%9D%B4%EC%9C%A0

 

setTimeout() - Web API | MDN

전역 setTimeout() 메서드는 만료된 후 함수나 지정한 코드 조각을 실행하는 타이머를 설정합니다.

developer.mozilla.org

 

위 문서에 나온 내용을 요약해보면

1. 중첩 타임아웃 사용시

브라우저는 setTimeout 호출이 5번 이상 호출된 경우 4ms의 최소 타임아웃을 강제함

2. 비활성탭의 타임아웃

백그라운드 탭으로 인한 부하(와 그로 인한 배터리 사용량)를 경감하기 위해, 브라우저는 비활성 탭에서의 최소 딜레이에 최소 값을 강제함

3. 페이지, 운영체제, 브라우저가 다른 작업으로 인해 바쁠 경우

페이지, 운영체제, 브라우저가 다른 작업으로 인해 바쁠 경우 타임아웃이 예상보다 늦게 실행될 수 있습니다.

 

나의 경우 3번에 해당하는 경우라고 추정하였다. (서버에서 데이터를 불러오고, 이를 이용하여 렌더링을 마치고 나서 스크롤이 생긴 이후 스크롤을 이동시켜야 하기 때문에)

 

이를 더 자세히 이해하기 위해서는 자바스크립트 런타임에 대한 이해가 필요한데, 이는 다음장에서 설명하도록 하겠다.

해결해야 하는 문제점

1. 당시 우리 서비스는 쇼핑몰 아이템을 클릭 후 뒤로가기 시 다시 서버에 요청을 보내 데이터를 받아와 화면을 갱신해야하는 요구사항이 있었기 때문에 앞서 소개했던 keep-alive라던가, web storage에 모든 데이터를 넣는 방법이라던가, scrollBehavior을 통해 스크롤 위치를 return하는 방법을 그대로 사용하기 어려웠다.

 

2. 또, 알 수 없이 페이지를 렌더링 할 때 스크롤이 맨 아래로 향해있는 이슈가 있어 window.scrollTo(0,0) 코드를 넣어 가장 상위로 스크롤을 강제로 위치시키고 있었다.

 

어떻게 해결했는가?

1. Vue Router meta 정보에 savePosition, preservePosition 이라는 속성(boolean)을 할당하여, 페이지가 넘어갈 시 스크롤 위치를 저장하는 페이지와 다시 돌아올 때 저장했던 스크롤 위치를 불러오는 페이지를 설정할 수 있게 했다.

 

2. scrollBehavior에서 뒤로가기를 할 시, 이전 scroll위치를 받아와 sessionStorage에 저장하였고, 뒤로가기가 아닐시에는 (0,0)을 반환하여 처음 위치로 가게 하였다.

 

3. 라이프사이클 updated 훅에서 nextTick을 이용하여 모든 화면(자식 컴포넌트 포함)이 렌더링 된 후 window.scrollTo를 이용하여 sessionStorage에서 저장한 이전 scroll위치로 스크롤을 위치시켰다.

*자식 컴포넌트를 포함한 모든 화면이 update가 된 이후에 메서드를 호출하려면 mounted가 아닌 updated의 nextTick 안에서 해야한다*

 

4. 알 수 없이 페이지의 스크롤이 맨 아래로 향해있었던 이슈는 DOM에서 computed로 계산된 유저의 이름을 바로 사용하고 있었는데, 그 부분을 data의 변수에 할당하고 computed가 아닌 data안의 변수를 DOM에서 사용하는 방식으로 변경하니 이슈가 해결되었다. 그런데 정확한 원인은 아직 제대로 파악하지 못하였다..

 

 

쇼핑몰 프로젝트를 진행하면서, 뒤로가기 시 스크롤 유지 관련한 이슈 해결 히스토리를 기록해보고자 이 글을 작성하게 되었다. 현재 프로젝트에서는 Vue + Vue Router + Vuex 를 사용한다.

 

 

개요

현재 진행하던 프로젝트에서는 기존에 sessionStorage에 스크롤 위치를 저장하고, updated 훅에서 모든 화면 update가 끝난 후 sessionStorage에 있는 scroll값을 가져와 scroll을 이동시키는 스크롤을 컨트롤 하고 있었다. 이과정에서 서버 통신 후 데이터를 렌더링하는 시점과, 스크롤을 이동시키는 시점에 오류가 생겨 스크롤을 잘 잡지 못하는경우가 발생하여 로직을 수정하게 되었다.

 

고려해본 방법들 

1. keep-alive 적용

2. web storage 사용

3. router savedPosition 사용

 

Keep-alive 적용

keep-alive는 컴포넌트를 전환할 때, 성능상의 이유로 상태를 유지하거나, 재 렌더링을 피할 수 있게 해주는 element입니다. 재 렌더링을 피할 수 있기 때문에, 뒤로가기 시 별다른 장치 없이 그대로 화면을 유지할 수 있는 장점이 있다. 

<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>

 

사용은 이런식으로 원하는 컴포넌트를 <keep-alive> 태그로 감싸주면 된다.

여기서 유의할점은 화면 전체의 재 렌더링을 피하고 싶다면 router-view 전체를 keep-alive 태그로 감싸야 한다는 것이다. (이것때문에 여러번 실패를 했었네요..) 

뒤로가기를 누를때만 적용 되야 하기 때문에 동적으로 할당 해야한다. 또한 재렌더링이 안되기 때문에 신중히 사용해야한다.

 

Web Storage 사용

화면 데이터와 스크롤 위치를 localStorage나 sessionStorage에 담아놓고, 돌아올 때 그대로 다시 불러오는 방법. 새롭게 서버통신을 해서 다시 리스트를 불러오지 않기 때문에 infinity scroll 사용 시 스크롤 위치를 잡기가 용이하다. 또 라이프사이클 훅에서 이루어지는 모든 것들이 정상 동작하므로 keep-alive를 사용할 수 없는 경우(라이프사이클을 거쳐야 하는 경우) 사용하기 좋다.

 

router savedPosition 사용

vue router는 scroll을 컨트롤 하기 위한 scrollBehavior 라는 함수를 제공하고 있다. 이는 HTML 히스토리 모드에서만 작동하는데, 라우터 객체를 만들때

const router = new VueRouter({
  routes: [...],
  scrollBehavior (to, from, savedPosition) {
    // 원하는 위치로 돌아가기
  }
})

이 안에 함수를 넣어서 사용하면 된다.

 

함수 안에서 리턴 값에 따라 스크롤의 위치가 변경되는데, 세번째 전달인자 (여기서는 savedPosition)은 브라우저가 앞/뒤 버튼으로 트리거 될때만 사용 가능하다

scrollBehavior (to, from, savedPosition) {
  if (savedPosition) {
    return savedPosition
  } else {
    return { x: 0, y: 0 }
  }
}

이런식으로 사용하면 되는데, 무조건 맨 위로 올리고 싶을 때는 

scrollBehavior (to, from, savedPosition) {
  return { x: 0, y: 0 }
}

이렇게 항상 0을 리턴하면 된다.

 

또 부드럽게 스크롤을 하도록 동작을 컨트롤 할 수도 있는데,

scrollBehavior (to, from, savedPosition) {
  if (to.hash) {
    return {
      selector: to.hash,
      behavior: 'smooth',
    }
  }
}

 이렇게 behavior에 스크롤 옵션을 입력 하면 된다. (selector는 위에서 설명했던 스크롤 위치이다)

 

 

이 이슈를 어떻게 해결하게 되었는지 전체적인 플로우는 다음장에서 설명하겠다.

현재 진행하고 있는 프로젝트는 첫화면에 많은 이미지 리소스를 요청한다. (쇼핑몰)

이때 최초에 lazy loading이 적용되어 있지 않아, 사용자가 보고 있지 않은 화면에 대한 리소스도 모두 요청해 가져오고 있었다. 이에 custom directive를 만들어 img태그에 이 directive를 추가하면 lazy loading이 적용될 수 있도록 만들어 누구나 적용할 수 있게 만들어 두었다. 현재 Vue2 를 이용하여 개발중이다.

 

Lazy Loading 이란?

페이지에서 실제로 필요할 때까지 리소스의 로딩을 미루는 것으로 웹 최적화에 많이 쓰인다. 실제로 사용자가 페이지를 보고 있는 타이밍에 이미지를 로딩한다. 또한 이미지 말고 다른 파일 (ex. js파일) 에도 적용 될 수 있다. 현재 이 글에서는 우선적으로 이미지만 다루려고 한다.

 

Custom Directive란?

vue에서는 v-show v-if 같은 기본 디렉티브가 주어져 손쉽게 Dom을 조작할 수 있다. 뿐만 아니라 기본 디렉티브 이외에 사용자가 커스텀하여 디렉티브를 제작할 수도 있는데, element를 매개변수로 받아 dom을 조작하는 커스텀 디렉티브를 만들어 사용 할 수 있다.

 

 

적용법

일단 img태그에 data-src라는 속성을 만들어서 이미지 리소스를 할당합니다. (속성 이름은 마음대로 하셔도 좋습니다)

이후 자바스크립트에서 제공하는 Intersection Observer API르르 이용하여 타겟 element와 document의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하여 타겟 element가 사용자의 viewport에 들어왔을 때, data-src에 할당 했던 이미지 리소스를 src속성에 재 할당하여 이미지를 로드하고, data-src 속성을 제거합니다.

 

Intersection Observer는 최신버전의 크롬과 사파리에서는 작동하지만 IE에서는 작동하지 않고 있음을 주의해야합니다. 저희 서비스는 모바일 웹앱 서비스로 크롬과 사파리로만 서비스 하고 있기 때문에 Intersection Observer를 사용하여 개발하는 것이 적절하다고 판단하여 아래와 같은 코드를 적용시켰습니다. 

 

 

directives: {

        lazyload: {

            inserted(el) {

                function imageLoad(targetElement) {

                    const imgElement = targetElement;

 

                    imgElement.setAttribute("src", imgElement.getAttribute("data-src"));

                    imgElement.onload = function () {

                        imgElement.removeAttribute("data-src");

                    };

                }

 

                function callIntersectionApi() {

                    const options = { root: null, threshold: 0.5, rootMargin: "0px" };

 

                    const lazyLoadCallback = (entries, observer) => {

                        entries.forEach((entry) => {

                            if (entry.isIntersecting) {

                                imageLoad(entry.target);

                                observer.unobserve(entry.target);

                            }

                        });

                    };

                    const lazyLoadingIO = new IntersectionObserver(lazyLoadCallback, options);

                    lazyLoadingIO.observe(el);

                }

                window.IntersectionObserver ? callIntersectionApi() : imageLoad(el);

            },

        },

    },

 

 

Intersection Observer를 생성 할때 options에 들어가는 넣은 요소들은 다음과 같습니다.

1. root : 대상 객체의 가시성을 확인할 때 사용되는 뷰포트 요소입니다. 이는 대상 객체의 조상 요소여야 합니다. 기본값은 브라우저 뷰포트이며, root 값이 null 이거나 지정되지 않을 때 기본값으로 설정됩니다.

 

2. threshold : observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. 만일 50%만큼 요소가 보여졌을 때를 탐지하고 싶다면, 값을 0.5로 설정하면 됩니다. 기본값은 0이며(이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다). 1.0은 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미합니다.

 

3. rootMargin : root 가 가진 여백입니다. 이 속성의 값은 CSS의 margin 속성과 유사합니다. e.g. "10px 20px 30px 40px" (top, right, bottom, left). 이 값은 퍼센티지가 될 수 있습니다. 이것은 root 요소의 각 측면의 bounding box를 수축시키거나 증가시키며, 교차성을 계산하기 전에 적용됩니다. 기본값은 0입니다.

 

 

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API

 

Intersection Observer API - Web API | MDN

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.Intersection Observer API는 타겟 요소와 상위 요소 또는

developer.mozilla.org

여기서 더 많은 예제를 보실수 있습니다 :)

 

SPA앱 프로젝트를 진행하면서 앱의 기능이 많아져 bundle의 크기가 점점 증가해 초기로딩이 오래걸리는 문제가 생겼고, 이를 해결 하기위해 code splitting을 적용했습니다.

 

현재 제가 하고있는 프로젝트는 vue2 + CLI 3 입니다.

 

개봘 환경 조건은 

1. 싱글 파일 컴포넌트 체계(.vue)

2. 웹팩 - 모듈 번들러 (2.x이상)

3.바벨 Syntax-dynamic-import입니다

 

CLI로 생성한 경우 1,2번은 만족하고 있지만 3번은 별도 설치가 필요합니다.

 

1. npm으로 설치해 줍니다.

npm install --save-dev babel-plugin-syntax-dynamic-import

2. .babelrc, 혹은 babel.config 파일에 플러그인을 추가합니다.

{
  "plugins": ["syntax-dynamic-import"]
}
3. CLI3를 사용하고 계신다면 webpack의 prefetch preload 플러그인을 삭제해야합니다. 그렇지 않으면 분리된 chunk파일들을 미리 받아오기 때문입니다.
저는 vue.config.js를 사용중이며
  chainWebpack: (config) => {
        config.plugins.delete("prefetch");
        config.plugins.delete("preload");
    },

를 추가하여 prefetch 와 preload 기능을 껐습니다.

 

4. 라우터 파일에 들어가 코드를 수정합니다.

 

기본으로 import 했던 아래의 방식과는 다르게

import MainMenu from "@/views/main/Main.vue";
동적으로 import 하는 아래의 방식으로 변경합니다!
{
	path: 'url 이름',
	component: () => import('컴포넌트 이름')
}
 
여기서 chunk파일들을 load하는것을 조절하고 싶다면 아래처럼 webpackChunkName에 공통적으로 같은 이름을 부여하면 이 이름을 가진 chunk가 최초로 로드되는 순간 같은 이름을 설정한 chunk들이 함께 로드되게 됩니다.
또한 chunk파일 이름이 여기서 설정한 이름으로 만들어집니다. 

const MainMenu = () => import(/* webpackChunkName: "MainMenu" */ "@/views/main/Main.vue");

 

최근 프로젝트가 점점 커지면서 초기 로딩시간이 너무 지연되어 번들 크기를 최적화 하려는 노력을 몇가지 해보았다.

그중 하나는 webpack-bundle-analyzer 사용하여 번들의 크기를 크게 만드는 원인을 파악하여 사이즈를 줄이는 것이다.

 

현재 사용하는 프로젝트는 vue버전 2와 vue cli 버전3을 이용하고 있다. 패키지매니저는 npm 을 사용한다.

 

일단 npm을 이용하여 webpack-bundle-analyzer을 설치한다. 이때 dev모드로 설치 함을 잊지말자

 

 

npm installl --save-webpack-bundle-analyzer

 

이후 webpack.config.js 파일에 들어가서

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

이렇게 입력 해 주면된다.

 

나는 vue.config.js 를 사용하기 때문에 vue.config.js 밑에

 configureWebpack: {
        plugins: [new BundleAnalyzerPlugin()],
    },

를 추가하였다.

 

이후 npm 으로 run을 시키면 다음과 같이 현재 번들이 어떻게 이루어져 있는지 시각적으로 볼 수 있다.

 

+ Recent posts