해결해야 하는 문제점

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에서 사용하는 방식으로 변경하니 이슈가 해결되었다. 그런데 정확한 원인은 아직 제대로 파악하지 못하였다..

 

 

자바스크립트에서 배열의 중복을 제거하는 가장 쉬운 방법은 Set객체를 이용하는 것이다

프로그래머스의 폰켓몬 문제로 예시를 들어보자

 

 

 

 

문제 설명

당신은 폰켓몬을 잡기 위한 오랜 여행 끝에, 홍 박사님의 연구실에 도착했습니다. 홍 박사님은 당신에게 자신의 연구실에 있는 총 N 마리의 폰켓몬 중에서 N/2마리를 가져가도 좋다고 했습니다.
홍 박사님 연구실의 폰켓몬은 종류에 따라 번호를 붙여 구분합니다. 따라서 같은 종류의 폰켓몬은 같은 번호를 가지고 있습니다. 예를 들어 연구실에 총 4마리의 폰켓몬이 있고, 각 폰켓몬의 종류 번호가 [3번, 1번, 2번, 3번]이라면 이는 3번 폰켓몬 두 마리, 1번 폰켓몬 한 마리, 2번 폰켓몬 한 마리가 있음을 나타냅니다. 이때, 4마리의 폰켓몬 중 2마리를 고르는 방법은 다음과 같이 6가지가 있습니다.

  1. 첫 번째(3번), 두 번째(1번) 폰켓몬을 선택
  2. 첫 번째(3번), 세 번째(2번) 폰켓몬을 선택
  3. 첫 번째(3번), 네 번째(3번) 폰켓몬을 선택
  4. 두 번째(1번), 세 번째(2번) 폰켓몬을 선택
  5. 두 번째(1번), 네 번째(3번) 폰켓몬을 선택
  6. 세 번째(2번), 네 번째(3번) 폰켓몬을 선택

이때, 첫 번째(3번) 폰켓몬과 네 번째(3번) 폰켓몬을 선택하는 방법은 한 종류(3번 폰켓몬 두 마리)의 폰켓몬만 가질 수 있지만, 다른 방법들은 모두 두 종류의 폰켓몬을 가질 수 있습니다. 따라서 위 예시에서 가질 수 있는 폰켓몬 종류 수의 최댓값은 2가 됩니다.
당신은 최대한 다양한 종류의 폰켓몬을 가지길 원하기 때문에, 최대한 많은 종류의 폰켓몬을 포함해서 N/2마리를 선택하려 합니다. N마리 폰켓몬의 종류 번호가 담긴 배열 nums가 매개변수로 주어질 때, N/2마리의 폰켓몬을 선택하는 방법 중, 가장 많은 종류의 폰켓몬을 선택하는 방법을 찾아, 그때의 폰켓몬 종류 번호의 개수를 return 하도록 solution 함수를 완성해주세요.

제한사항
  • nums는 폰켓몬의 종류 번호가 담긴 1차원 배열입니다.
  • nums의 길이(N)는 1 이상 10,000 이하의 자연수이며, 항상 짝수로 주어집니다.
  • 폰켓몬의 종류 번호는 1 이상 200,000 이하의 자연수로 나타냅니다.
  • 가장 많은 종류의 폰켓몬을 선택하는 방법이 여러 가지인 경우에도, 선택할 수 있는 폰켓몬 종류 개수의 최댓값 하나만 return 하면 됩니다.

 

 

 

내가 작성한 답안은 다음과 같다

 

function solution(nums) {
    var answer = 0;
    let limitNumber = nums.length/2

    let newNums = [... new Set(nums)]


    return newNums.length > limitNumber ? limitNumber : newNums.length;
}

 

 전달받은 배열을 Set객체로 만들어 중복없는 데이터로 표현하고, 이를 다시 배열에 담아 중복없는 배열을 만들면 위처럼 쉽게 해결할 수 있다.

쇼핑몰 프로젝트를 진행하면서, 뒤로가기 시 스크롤 유지 관련한 이슈 해결 히스토리를 기록해보고자 이 글을 작성하게 되었다. 현재 프로젝트에서는 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을 시키면 다음과 같이 현재 번들이 어떻게 이루어져 있는지 시각적으로 볼 수 있다.

 

다음은 카카오 로그인 릴리즈 해시키를 등록해보겠습니다.

 

일단 윈도우에서 릴리즈 해시키를 쉽게 얻으려면 크롬에서 제공해주는 리눅스 터미널 TermLinux를 사용해야합니다.

https://chrome.google.com/webstore/detail/termlinux-terminal-for-co/hlgjjjociconbkooaggfmjhalogadcee

 

TermLinux 명령 행을위한 터미널

소형 온라인 리눅스 가상 머신에 명령 행 콘솔을 제공하는 웹 터미널

chrome.google.com

1편에서 얻었던 SHA-1키를 복사한후 TermLinux 터미널에 

echo <SHA-1 키 입력> | xxd -r -p | openssl base64

명령어를 입력합니다.

 

명령어 입력후 나온 해시키를 복사하여

카카오 개발자 홈페이지 앱설정 -> 플랫폼 -> 키 해시에 복사한 해시키를 붙여 넣습니다.

 

플랫폼 선택
키 해시 추가

이제 완료되었으니 릴리즈로 테스트 해봅니다!

안드로이드 앱을 릴리즈 버전에서 구글로그인, 카카오 로그인 api를 사용하기 위해서는 

릴리즈키를 얻어 개발자 콘솔에 등록해야 합니다.

 

1. 릴리즈 키스토어를 생성합니다.

 

2. 커맨드창을 열고 jdk/bin 디렉토리로 이동합니다

keytool -list -v -keystore <키스토어 패스>

ex)

대부분의 jdk 디렉토리는 program files\java\밑에 있습니다

그리고 명령어를 입력해줍니다.

keytool -list -v -keystore <키스토어 path>

키스토어 path는 build -> Generate Signed Bundle or APK 를 선택하여 확인하실 수 있습니다.

결과로 받은 SHA1 을 복사해 줍니다.

SHA1을 복사하여 Google Cloud Platform 혹은 Firebase Console에  등록합니다.

 

Google Cloud Platform -> API 및 서비스 -> 사용자 인증 정보 선택 -> 사용자 인증 정보 만들기 -> android 선택 후 

패키지 명과 SHA-1 칸에 복사해둔 SHA1키를 붙여넣고, 저장을 누릅니다.

이제 연동이 끝났으니 릴리즈 버전에서 테스트를 해봅니다!

안드로이드에서 애플 로그인을 구현하려면 파이어베이스와 연동이 필요합니다.

★진행하기에 앞서서 파이어베이스에 프로젝트를 추가 해주세요!!★

 

1. 애플 개발자 사이트에 앱 설정하기

1-1. 앱 등록

위와 같은 순서로 진행 해주세요

 

 

1-2. 키 등록

로그인을 체크하고 configure 버튼을 눌러주세요

 

앞서 만든 앱 ID를 선택하고 configure를 save 해주세요
키를 만들고 다운로드 해주세요, 비공개키는 안전하게 프로젝트 디렉토리안에 옮겨둡시다

 

1-3. 서비스 ID 등록

 

순서대로 진행하여 service id를 생성해주세요

 

2. 파이어베이스 연동하기

authentication -> apple 로그인을 사용설정 해줍시다.

앞서 등록한 service id, key, 앱의 id를 각각 입력해주시고, 다운받은 비공개 키를 열어 복사 붙여넣기 해줍니다!

그리고 스위치를 on 으로 변경한 후 저장을 눌러주세요

 

구현 코드는 아래 사이트를 참고 해주세요.

https://firebase.google.com/docs/auth/android/apple?hl=ko 

 

Android에서 Apple을 통해 인증  |  Firebase

Firebase SDK를 사용하여 엔드 투 엔드 OAuth 2.0 로그인 과정을 실행하면 사용자가 Apple ID를 사용해 Firebase에 인증하도록 할 수 있습니다. 중요: Apple로 로그인하려면 사용자는 다음을 충족해야 합니다

firebase.google.com

 

1. 네이버 안드로이드 플랫폼 등록하기 

네이버 개발자 사이트 접속 https://developers.naver.com/ -> 내 애플리케이션 등록 선택

제공받을 정보를 선택하고 애플리케이션 등록하기
패키지명과 다운로드 URL을 입력해줍시다!!

2. Gradle에 라이브러리 추가하기

SDK를 다운로드 합시다!!

SDK 다운로드 후 .arr 파일을 프로젝트 libs 폴더 아래 추가 해주세요!

그 이후 module gradle 파일 dependency에


implementation files('libs/naveridlogin_android_sdk_4.2.6.aar')를 추가해줍시다!

 

3. 난독화 설정 제외

proguard-project.txt 파일에 

keep public class com.nhn.android.naverlogin.** { public protected *; } 코드를 추가하여 난독화 설정을 제외 해주세요

 

네이버 로그인 개발 가이드는 아래 링크를 참고해주세요!

https://developers.naver.com/docs/login/devguide/devguide.md#%EB%84%A4%EC%9D%B4%EB%B2%84%EC%95%84%EC%9D%B4%EB%94%94%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B0%9C%EB%B0%9C%EA%B0%80%EC%9D%B4%EB%93%9C

+ Recent posts