본문으로 건너뛰기

나를 위한 면접 (ALL)

· 약 21분

실무에 치이느라 기본적인 걸 까먹을까봐 정리해보자 정보처리 에서 컴퓨터 기초를 보면 되고. 지극히 주관적이라 정답이 아닐 수 있습니다

수학

미적분

편미분까진 안물어보더라

베이지안 확률

사후확률 = 가능도 * 사전확률 / 증거

// 붉은 점이 얼굴에 보일 때 수두 환자일 확률은 어떻게 구할까?
P(수두 | 붉은점) = P(붉은 점 | 수두) * P(수두) / P(붉은점)

자료구조

B Tree

m-원 트리의 단점임 한쪽으로 편중된 트리를 생성되는 경우를 보완하고자 루트노드로부터 리프노드의 레벨을 같도록 유지한 트리

BASIS FOR COMPARISONB-TREEBINARY TREE
Essential constraintA node can have at max M number of child nodes(where M is the order of the tree).A node can have at max 2 number of subtrees.
UsedIt is used when data is stored on disk.It is used when records and data are stored in RAM.
Height of the treelogM N (where m is the order of the M-way tree)log2 N
ApplicationCode indexing data structure in many DBMS.Code optimization, Huffman coding, etc.

space complexity B-tree is O(n). Insertion and deletion time complexity is O(logn).

출처

B+ Tree

B+은 index node와 data node로 구성

출처

B* Tree

B-tree의 경우에 각 노드가 최소한 반 이상 차 있어야 하는 조건각 노드가 최소한 2/3이상 차있어야 한다로 변경하면 이것이 B*tree이다

공간 활용도를 높일 수 있다. 출처

Binary Tree

모든 노드의 차수가 2 이상을 넘지 않는 트리를 말한다, 왼쪽자식노드는 부모노드의 값보다 작이야하며 오른쪽 자식노드는 부모노드의 값보다 커야한다.

Binary Search Tree

이진탐색(binary search)과 연결리스트(linked list)를 결합한 자료구조의 일종입니다. 이진탐색의 효율적인 탐색 능력을 유지하면서도, 빈번한 자료 입력과 삭제를 가능하게끔 고안됐습니다.

  • 모든 원소는 서로 다른 유일한 키를 가짐
  • 왼쪽 서브트리에 있는 원소들의 값은 그 루트의 값보다 작음
  • 오른쪽 서브트리에 있는 원소의 값들은 그 루트의 값보다 큼
  • 왼쪽 서브트리와 오른쪽 서브트리도 이진 탐색 트리임

O(n), 완전 이진 트리에 가까울 수록 O(log2(n))

출처

Insertion Sort

앞에서부터 하나씩 비교하여 위치를 삽입

O(n^2)

function insertionSort(unsortedList) {
const len = unsortedList.length;
for (let i = 1; i < len; i++) {
// Copy of the current element.
const tmp = unsortedList[i];
// Check through the sorted part and compare with the number in tmp. If large, shift the number
for (var j = i - 1; j >= 0 && unsortedList[j] > tmp; j--) {
// Shift the number
unsortedList[j + 1] = unsortedList[j];
}
// Insert the copied number at the correct position
// in sorted part.
unsortedList[j + 1] = tmp;
}
}

출처

Selection Sort

하나씩 뒤로 비교하여 최소값을 맨 앞으로 삽입

O(n^2)

function selectionSort(items) {
const length = items.length;
for (let i = 0; i < length - 1; i++) {
// Number of passes
// min holds the current minimum number position for each pass; i holds the Initial min number
let min = i;
// Note that j = i + 1 as we only need to go through unsorted array
for (let j = i + 1; j < length; j++) {
// Compare the numbers
if (items[j] < items[min]) {
// Change the current min number position if a smaller num is found
min = j;
}
}
if (min != i) {
// After each pass, if the current min num != initial min num, exchange the position.
// Swap the numbers
const tmp = items[i];
items[i] = items[min];
items[min] = tmp;
}
}
}

출처

Quick Sort

pivot을 하나 뽑는다 보통 list.length / 2 -1 을 선택한다. pivot 앞에는 pivot보다 작게, pivot 뒤에는 pivot보다 크게 값을 바꿔치고 재귀를 돌린다

최악 O(n^2), 평균 O(nlogn)

const quickSort = (list) => {
if (list.length < 2) {
return list;
}

const pivot = list[0];
// const pivot = list[Math.floor(list.length / 2)];
const smaller = list.filter((item) => item < pivot);
const bigger = list.filter((item) => item > pivot);

return [...quickSort(smaller), pivot, ...quickSort(bigger)];
};

참고

List

Linked List

List vs Linked List

리스트가 대부분 좋다 연결 리스트는 중간에 삽입 삭제시 좋다 리스트는 끝에 넣을 떄 좋다

연결 리스트는 검색시 링크를 따라가므로 비효율적이다. 리스트가 순차라 검색시에 좋다. 공간의 효율성은 연결 리스트

효율성 비교

function LinkedList() {
this.head = null;
}

LinkedList.prototype.push = function (val) {
const node = {
value: val,
next: null,
};

if (!this.head) {
this.head = node;
} else {
current = this.head;
while (current.next) {
current = current.next;
}
current.next = node;
}
};

const sll = new LinkedList();
sll.push(2);
sll.push(3);
sll.push(4);

// check values by traversing LinkedList
sll.head; // Object {data: 2, next: Object}
sll.head.next; // Object {data: 3, next: Object}
sll.head.next.next; // Object {data: 4, next: null}

출처

Graph

  • 그래프(Graph)는 연결되어 있는 원소간의 관계를 표현하는 자료구조이다. 나와 연관된 인간 관계를 나타내는 인맥 지도, 수도 배관에 대한 배수 시스템, 물질의 분자 구조 등은 연결 구조가 다양하기 때문에 선형 자료 구조나 트리로는 표현 할 수가 없다.
  • 그래프는 연결할 객체를 나타내는 정점(vertex)와 객체를 연결하는 간선(edge)의 집합으로 구성된다. 그래프 G를 *G=(V,E)*로 정의하는데, V는 그래프에 있는 정점들의 집합을 의미하고, E는 정점을 연결하는 간선들의 집합을 의미한다.

출처

// Implement a Graph
// basic operations:
// - add vertex (node)
// - add edge (node -> node)

function GraphNode(val) {
this.val = val;
this.edges = {};
}

function Graph() {
this.vertices = {};
}

// O(1) operation
Graph.prototype.addVertex = function (val) {
// add vertex only if it does not exist.
if (!this.vertices[val]) {
this.vertices[val] = new GraphNode(val);
}
};

// O(E) operation - edges
Graph.prototype.removeVertex = function (val) {
if (this.vertices[val]) {
// once you remove a vertex, you need to remove all edges pointing
// to the vertex.
delete this.vertices[val];

Object.keys(this.vertices).forEach(
function (key, index) {
if (this.vertices[key].edges[val]) {
delete this.vertices[key].edges[val];
}
}.bind(this),
);
}
};

// O(1) operation
Graph.prototype.getVertex = function (val) {
return this.vertices[val];
};

// O(1) operation
Graph.prototype.addEdge = function (start, end) {
// check to see if vertices exists.
// if it exists, set the edges and be done.
if (this.vertices[start] && this.vertices[end]) {
// check to see if edge exists, if it does, increment it's weight
if (this.vertices[start].edges[end]) {
this.vertices[start].edges[end].weight += 1;
} else {
// edge does not exist, set weight to 1.
this.vertices[start].edges[end] = { weight: 1 };
}
}
};

// O(1) operation
Graph.prototype.removeEdge = function (start, end) {
if (this.vertices[start] && this.vertices[end]) {
if (this.vertices[start].edges[end]) {
delete this.vertices[start].edges[end];
}
}
};

// O(1) operation
Graph.prototype.getEdge = function (start, end) {
return this.vertices[start].edges[end] || null;
};

Graph.prototype.neighbors = function (val) {
return this.vertices[val] ? this.vertices[val].edges : null;
};

const graph = new Graph();

graph.addVertex(5);
graph.addVertex(2);
graph.addVertex(6);
graph.addVertex(7);
graph.addEdge(2, 5);
graph.addEdge(6, 7);
graph.addEdge(7, 5);
console.log(graph.getEdge(2, 5));
console.log(graph.getEdge(6, 7));
graph.removeVertex(5);
console.log(graph.getEdge(2, 5));
console.log(graph.neighbors(6));
console.log(graph.neighbors(5));

출처

알고리즘

시간복잡도

  • 알고리즘이 문제를 해결하는 데에 얼마나 걸리는 지를 분석하는 것
  • 프로그램을 실행시켜 완료하는데 걸리는 시간을 의미

빅오표기법

  • 실행 시간은 최대한 이만큼 커지지만 더 천천히 커질 수도 있다는 의미인 점근적 표기법 형태를 사용하는 것 (점근 표기법)
  • 최악의 경우를 표기하는 방법

빅오에서 n이 의미하는것

  • 입력 데이터 개수

최단거리 알고리즘

  • 다익스트라 알고리즘
  • A* 알고리즘

슬라이딩 윈도우

이 문제가 도움 될 듯

데이터베이스

정규화

  • 관계형 데이터베이스의 설계에서 중복을 최소화하게 데이터를 구조화하는 프로세스

목적

  • 이상이 있는 관계를 재구성하여 작고 잘 조직된 관계를 생성하는 것에 있다.
  • 일반적으로 정규화란 크고, 제대로 조직되지 않은 테이블들과 관계들을 작고 잘 조직된 테이블과 관계들로 나누는 것을 포함한다.

비정규화

  • 과도한 정규화로 인해서 테이블의 수가 증가하게 되면, 다수의 JOIN이 발생함에 따라서 성능 저하가 발생할 수 있다. 보통의 경우 정규화 과정을 모두 거친 다음 마지막 단계에서 비정규화를 실시한다.
  • 단, 테이블을 합치는 것만이 비정규화는 아니다.

인덱싱 자료구조

B-Tree 알고리즘은 가장 일반적으로 사용되는 인덱스 알고리즘으로서, 상당히 오래전에 도입된 알고리즘이며 그만큼 성숙해진 상태입니다. B-Tree 인덱스는 칼럼의 값을 변형하지 않고, 원래의 값을 이용해 인덱싱하는 알고리즘 입니다.

R-Tree 다차원 공간

Hash 인덱스 알고리즘은 컬럼의 값으로 해시 값을 계산해서 인덱싱하는 알고리즘으로, 매우 빠른 검색을 지원합니다. 하지만 값을 변형해서 인덱싱하므로, 전방(Prefix) 일치와 같이 값의 일부만 검색하고자 할 때는 해시 인덱스를 사용할 수 없습니다. Hash 인덱스는 주로 메모리 기반의 데이터베이스에서 많이 사용합니다.

Fractal-Tree 알고리즘은 B-Tree의 단점을 보완하기 위해 고안된 알고리즘입니다. 값을 변형하지 않고 인덱싱하며 범용적인 목적으로 사용할 수 있다는 측면에서 B-Tree와 거의 비슷하지만 데이터가 저장되거나 삭제될 때 처리 비용을 상당히 줄일 수 있게 설계된 것이 특징입니다. 아직 B-Tree 알고리즘만큼 안정적이고 성숙되진 않았지만 아마도 조만간 B-Tree 인덱스의 상당 부분을 대체할 수 있지 않을까 생각합니다.

출처

캐싱시 자료구조

  • Hash Table and a Linked List

디비 클러스터링

개념 참조 운영 참조

Isolation level

  • Read Uncommitted
  • Read Committed
  • Repeatable Read
  • Serializable

개념 참조 이하 참조

Read Uncommitted Isolation Level

SELECT 문장을 수행하는 경우 해당 데이터에 Shared Lock이 걸리지 않는 Level입니다. 따라서, 어떤 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는 B라는 아직 완료되지 않은(Uncommitted 혹은 Dirty) 데이터 B를 읽을 수 있습니다.

Read Committed Isolation Level

SQL Server가 Default로 사용하는 Isolation Level입니다. 이 Level에선 SELECT 문장이 수행되는 동안 해당 데이터에 Shared Lock이 걸립니다. 그러므로, 어떠한 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는 해당 데이터에 접근할 수 없습니다.

Repeatable Read Isolation Level

트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 Shared Lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정이 불가능합니다. 가령, Select col1 from A where col1 between 1 and 10을 수행하였고 이 범위에 해당하는 데이터가 2건이 있는 경우(col1=1과 5) 다른 사용자가 col1이 1이나 5인 Row에 대한 UPDATE이 불가능합니다. 하지만, col1이 1과 5를 제외한 나머지 이 범위에 해당하는 Row를 INSERT하는 것이 가능합니다.

Serializable Isolation Level

트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 Shared Lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정 및 입력이 불가능합니다. 예를 들어, Repeatable Read의 경우 1에서 10 사이에 해당되는 데이터에 대한 UPADTE이 가능하였습니다. 하지만 이 Level에서는 UPDATE 작업도 허용하지 않습니다.

ACID

원자성, 일관성, 독립성, 영속성

Atomicity

트랜잭션 내의 명령은 반드시 완벽히 수행 모두가 수행되거나 오류시 전부가 취소되어야함

Consistency

DB의 전체 요소는 트랜잭션 수행 전과 트랜잭션 수행 완료 후의 상태가 같아야함 (질량 보존 법칙처럼)

Isolation

둘 이상의 트랜잭션이 병행 실행되는 경우 다른 트랜잭션 연산이 끼어들 수 없음 수행 중인 트랜잭션은 완료될 때 까지 다른 트랜잭션에서 참조 불가.

Durability

시스템이 고장나도 영구적으로 반영

CRUD

Create(생성), Read(읽기), Update(갱신), Delete(삭제)

InnoDB와 MyISAM의 차이

image from hexo

MariaDB가 MySQL보다 나은점

증분 백업

정해진 시간을 기준으로 그 이후에 변경된 파일만을 백업하는 방식

서버

서버 로드밸런싱

설명할 수 있음

이미지 클러스터링

k-means (피벗 추출 후 2진수 비교인데 아직 이해하기가 힘들다, 18년 말에는 딥러닝을 파보자!)

Build Process 설명

Deploy Server

Git Flow

브랜치 관리 전략

  • feature: develop에서 분기 후 기능 개발 후 develop으로
  • develop: 개발 브랜치
  • release: master로의 merge를 위한 준비
  • hotfix: master의 버그 픽스 후 develop과 master로 빠른 수정
  • master: 프로덕션 브랜치

image from hexo

참고

WAS 세팅 및 튜닝

프론트앤드

Virtual DOM

DOM 조작의 실제 문제는 각 조작이 레이아웃 변화, 트리 변화와 렌더링을 일으킨다는겁니다.

Virtual DOM 은 변화가 일어나면 그걸 오프라인 DOM 트리에 적용시키죠. 이 DOM 트리는 렌더링도 되지 않기때문에 연산 비용이 적어요. 연산이 끝나고나면 그 최종적인 변화를 실제 DOM 에 던져주는거에요. 딱 한번만 한는거에요.

사실, 이 과정은 Virtual DOM 이 없이도 이뤄질수 있어요. 그냥, 변화가 있을 때, 그 변화를 묶어서 DOM fragment 에 적용한 다음에 기존 DOM 에 던져주면 돼요.

그러면, Virtual DOM 이 해결 하려고 하는건 무엇이냐? 그 DOM fragment를 관리하는 과정을 수동으로 하나하나 작업 할 필요 없이, 자동화하고 추상화하는거에요. 그 뿐만 아니라, 만약에 이 작업을 여러분들이 직접 한다면, 기존 값 중 어떤게 바뀌었고 어떤게 바뀌지 않았는지 계속 파악하고 있어야하는데 (그렇지 않으면 수정 할 필요가 없는 DOM 트리도 업데이트를 하게 될 수도 있으니까요), 이것도 Virtual DOM 이 이걸 자동으로 해주는거에요. 어떤게 바뀌었는지 , 어떤게 바뀌지 않았는지 알아내주죠.

마지막으로, DOM 관리를 Virtual DOM 이 하도록 함으로써, 컴포넌트가 DOM 조작 요청을 할 때 다른 컴포넌트들과 상호작용을 하지 않아도 되고, 특정 DOM 을 조작할 것 이라던지, 이미 조작했다던지에 대한 정보를 공유 할 필요가 없습니다. 즉, 각 변화들의 동기화 작업을 거치지 않으면서도 모든 작업을 하나로 묶어줄 수 있다는거죠.

출처

ES6

SEO

Angular

React

Vue

Window에서 aws cli 사용시 bad interperter 오류

· 약 1분

git bash에서 aws cli 명령어를 실행시에 c:\Program Files... bad interpreter 오류가 나오면서 실행이 안 될 때는 다음과 같이 하면 된다.

해결

~/.bash_profile 에 aws alias를 추가한다.

$ vi ~/.bash_profile
## aws script 경로를 강제로 지정한다
alias aws='python "C:\Program Files\Python\Scripts\aws"'
## 추가 후 저장
$ source ~/.bash_profile

Cloudfront 캐시 지우기

· 약 1분

클라우드프론트로 static website hosting 을 할 경우에 삭제된 s3 의 파일이 노출되는 경우가 있다. 캐싱처리되어서 보이는 현상인데 다음과 같이 해결하면 된다.

해결

Invalidate 기능을 사용해서 캐시를 지워버리자. Cloudfront Distributions > ID 클릭 > Invalidations 에서 Create Invalidation 버튼을 누른다.

image from hexo

Object Paths에 캐시를 지울 경로를 입력한다. 예를 들어 assets/js 하위의 소스 맵 파일만 지운다면 다음과 같다.

/assets/js/*.map

입력 후에 Invalidate 버튼을 누르면 5~10 분 안에 리스트의 Status 가 Completed 로 변하고 기존 캐시가 사라진다.

pm2 앱에서 git hook시에 nginx 502 gateway timeout 오류

· 약 1분

원인

git pull을 hook으로 실행하는데 계속 nginx 502 gateway timeout 오류가 발생해 pm2 logs 앱 또는 nginx log를 계속 추적해도 별다른 에러 로그가 없었다. (exec: Internal Server error 라고만 적혀있었다.)

삽질 끝에 watch 속성을 사용하고 있는게 문제였다.

해결

ecosystem.config.js
module.exports = {
apps: [
{
name: "server",
script: "server.js",
env_production: {
NODE_ENV: "production",
},
watch: true,
ignore_watch: ["node_modules", ".git", "yarn.lock", "package-lock.json"],
exec_mode: "cluster",
instances: "max",
},
],
};

ignore_watch 속성에 .git 폴더와 다른 폴더들이 제대로 예외처리 되었는지 확인해보자.

nginx에 letsencrypt 인증서로 https 가장 빨리 적용하기

· 약 1분

centos7 기준

nginx 설치

$ vi /etc/yum.repos.d/nginx.repo

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=0
enabled=1

$ yum makecache
$ yum install -y nginx

$ vi /etc/nginx/conf.d/default.conf
## server_name에 도메인 연결

## 문법 체크
$ nginx -t

## nginx 시작
$ systemctl enable nginx
$ systemctl start nginx

certbot 설치

$ yum install -y epel-release
$ yum install -y certbot-nginx

## 인증
$ certbot --nginx -d my.domain.com

ssl nginx 설정

기존 포스트 nginx 연동 탭 참조

renew 설정

$ crontab -e

1 0 1 * * certbot renew --nginx

$ systemctl restart crond

systemctl restart nginx 하면 완료

리액트 네이티브 윈도우 네트워크 설정 (react-native)

· 약 2분

리액트 네이티브로 개발한 앱을 Expo로 구동 중 network response timed out 에러 메세지가 보인다면 다음과 같이 해결하면 된다.

방화벽

인바운드로 19000, 19001 포트를 열어줘야한다. 방화벽 > 고급 설정 > 인바운드 규칙 > 새 규칙 > 포트 에서 아래와 같이 설정해주고 이름은 react-native-expo 등 원하는 이름으로 규칙을 생성하자.

image from hexo

Hostname 설정

아이피 확인

ipconfig로 현재 Wifi가 설정된 내부 아이피 주소를 확인한다.

무선 LAN 어댑터 Wi-Fi:
연결별 DNS 접미사. . . . :
링크-로컬 IPv6 주소 . . . . :
IPv4 주소 . . . . . . . . . : 192.168.0.7
서브넷 마스크 . . . . . . . : 255.255.255.0
기본 게이트웨이 . . . . . . : 192.168.0.1

192.168.0.7 을 기억해 놓자.

설정 추가

REACT_NATIVE_PACKAGER_HOSTNAME 환경 변수를 설정해줘야한다.

Git Bash 환경에서는 다음과 같다.

export REACT_NATIVE_PACKAGER_HOSTNAME=192.168.0.7

실행

yarn start 후에 Expo 앱으로 QRCode를 찍으면 정상적으로 실행이 가능하다.

utf8 charset에서 emoji 필터링하기

· 약 2분

개요

utf8 charset 에서는 이모지를 처리할 수 없다. utf8mb4 언어셋 소개 및 표현범위 포스팅에 따르면 이모지가 mysql 또는 maria 의 utf8 셋의 가변공간을 사용하려 들려 하는 문제인데, utf8mb4 셋으로 변경하면 해결 된다.

mysql 이거나 maria 의 charset 을 변경하기 힘들 때 text 를 모두 replace 치면 된다. 시작해보자

php

writing a simple removeEmoji function stackoverflow 글에 좋은 함수가 있다.

소스

<?php
// https://stackoverflow.com/questions/12807176/php-writing-a-simple-removeemoji-function
function removeEmoji($text) {
$clean_text = "";
// Match Emoticons
$regexEmoticons = '/[\x{1F600}-\x{1F64F}]/u';
$clean_text = preg_replace($regexEmoticons, '', $text);
// Match Miscellaneous Symbols and Pictographs
$regexSymbols = '/[\x{1F300}-\x{1F5FF}]/u';
$clean_text = preg_replace($regexSymbols, '', $clean_text);
// Match Transport And Map Symbols
$regexTransport = '/[\x{1F680}-\x{1F6FF}]/u';
$clean_text = preg_replace($regexTransport, '', $clean_text);
// Match Miscellaneous Symbols
$regexMisc = '/[\x{2600}-\x{26FF}]/u';
$clean_text = preg_replace($regexMisc, '', $clean_text);
// Match Dingbats
$regexDingbats = '/[\x{2700}-\x{27BF}]/u';
$clean_text = preg_replace($regexDingbats, '', $clean_text);
// Match Flags
$regexDingbats = '/[\x{1F1E6}-\x{1F1FF}]/u';
$clean_text = preg_replace($regexDingbats, '', $clean_text);
// Others
$regexDingbats = '/[\x{1F910}-\x{1F95E}]/u';
$clean_text = preg_replace($regexDingbats, '', $clean_text);
$regexDingbats = '/[\x{1F980}-\x{1F991}]/u';
$clean_text = preg_replace($regexDingbats, '', $clean_text);
$regexDingbats = '/[\x{1F9C0}]/u';
$clean_text = preg_replace($regexDingbats, '', $clean_text);
$regexDingbats = '/[\x{1F9F9}]/u';
$clean_text = preg_replace($regexDingbats, '', $clean_text);
return $clean_text;
}
<?php
$textWithEmoji = 'thumbs up👍👍';
$text = removeEmoji($textWithEmoji);

// text => thumbs up

node

emoji-regex 라이브러리를 쓰면 된다.

yarn add emoji-regex

소스

const emojiRegex = require("emoji-regex");

// const regex = emojiRegex()
const textWithEmoji = "thumbs up👍👍";
const text = textWithEmoji.replace(emojiRegex(), "");

// text => thumbs up

여담

이모지처리 (붙히고 제거하고), linkify 를 합친 통합 모듈이 있다면 다운로드 수가 좀 될 것 같은데.

Docker로 스웨거 설치 후 테마 설정하기 (swagger)

· 약 2분

Swagger UISwagger Editor를 Docker 로 띄워서 빠르게 사용해보자. Swagger Hub에서 작업하는건 한글 입력이 잘 되지 않는 경우가 있었고 OAS3를 사용할 겸 설치하는 게 편하다.

설치

설치는 서버 하나만 있으면 충분하다.

Editor

$ docker run -d \
-p 7000:8080 \
--restart always \
--name swagger-editor \
swaggerapi/swagger-editor

UI

UI 에서는 API 문서를 상대경로로 사용하기 위해 볼륨 마운트를 했다. Docker for Windows에서 c 드라이브를 마운트하기 위해서는 아래처럼 //c/path 로 접근하면 된다.

$ docker run -d \
-p 9010:8080 \
-v //c/swagger-spec:/usr/share/nginx/html/spec \
--restart always \
--name swagger-ui \
swaggerapi/swagger-ui

테마 추가

테마는 나이스한 라이브러리를 사용하자. 두 컨테이너 모두 /usr/share/nginx/html/ 경로의 index.html을 열어 head 안에 넣어주면 된다.

docker exec로 쉘에 접근하는 건 다 하시리라 믿고 다만, 컨테이너에 bash 쉘이 없기에 기본 sh 쉘로 접근하면 된다.

index.html
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/themes/3.x/theme-material.css"
/>

여담

스웨거에 side menu 가 있는 버전이나 라이브러리가 있으면 좋을텐데, 누가 만들었을 것 같은데 아직 못 찾았다.

PWA - 서비스 워커 웹 캐싱 (Web Caching)

· 약 16분

개요

Progressive Web App의 시대가 왔다. Client단에서 모든 static 파일을 브라우저에 캐시를 할 수 있고 웹을 앱처럼 오프라인에서 작업하게 할 수 있다. 그 첫번째로 캐싱에 대해 알아보자.

준비

Promise, Fetch, Worker 및 Javascript 의 실행 구조, DOM 에 대한 사전지식이 필히 있어야 한다. HTTP Cache 를 걸어봤다면 이해가 쉬울 듯 싶다.

ServiceWorker

서비스 워커는 브라우저가 백그라운드에서 실행하는 스크립트이며, 클라이언트에 설치되는 프록시다. 이 개념은 중요해서 외워야한다. 브라우저 백그라운드에서 네트워크를 가로채는 Thread 라고 보면 된다.

웹 캐싱 뿐만 아니라 백그라운드 동기화, 웹 푸쉬 등의 기능을 처리할 수 있다. 궁금하다면 여기를 들어가보자. (포스팅을 다 읽고 들어가보는 걸 추천한다.)

웹 캐싱은 CacheStorage를 사용한다. Sqlite 같은 클라이언트 데이터베이스인데, Key:value 로 구성된 데이터베이스라고 보면 된다.

주의

https 환경 또는 localhost 도메인에서만 이 기능을 사용할 수 있다.

브라우저별 지원상황은 다음과 같다 1803기준 Safari에도 Shipped가 되었다! 1712 기준 Safari가 아직도 Developement 상태인게 조금 아쉽다

세팅

sw.js란 파일을 public폴더(index.html이 있는)에 생성하자. 그리고 메인 script파일에 다음과 같이 service-worker를 불러오는 구문을 추가한다.

// navigator (브라우저)에 serviceWorker 기능이 있는지 확인
if ("serviceWorker" in navigator) {
// 서비스워커 설치시 DOM 블로킹을 막아준다.
window.addEventListener("load", function () {
// 서비스워커를 register 하면 promise를 반환한다.
navigator.serviceWorker
.register("/sw.js")
.then(() => {
console.log("서비스 워커가 등록되었다.");
})
.catch((error) => {
console.log(error);
});
});
}

개발자도구를 열고 Application > ServiceWorkers 탭으로 가면 설치가 된 것을 확인할 수 있다.

image from hexo

이렇게 뜨면 설치된 것이다.

Basic

서비스워커에서는 self 키워드로 자기 자신을 접근할 수 있다. 몇 가지 static 파일들을 캐싱처리 해보자. 모던 브라우저에서만 지원이 되므로 arrow function을 사용해도 된다.

sw.js
const PRE_CACHE_NAME = "캐시-스토리지1";
// 캐시하고 싶은 리소스
const urlsToCache = [
"/public/image/image1.png",
"/public/css/font-awesome.min.css",
];

// 서비스워커가 설치될 때
self.addEventListener("install", (event) => {
// 캐시 등록 이벤트가 끝날 때까지 기다려
event.waitUntil(
// '캐시-스토리지1'을 연다.
// @return {Promise} 연결된 Cache Database를 반환한다.
caches
.open(PRE_CACHE_NAME)
.then((cache) => {
console.log("캐시 디비와 연결됨");
// addAll 메소드로 내가 캐싱할 리소스를 다 넣어주자.
return cache.addAll(urlsToCache);
})
.then(() => {
// 설치 후에 바로 활성화 단계로 들어갈 수 있게 해준다.
return self.skipWaiting();
}),
);
});

이렇게 추가해주고 개발자도구의 Application 탭으로 가서 좌측 메뉴의 Cache Storage 를 새로고침 하면 방금 추가한 캐시-스토리지1에 내 이미지와 css가 등록된 걸 확인할 수 있다.

그리고 개발자도구의 Network 탭에서 호출하는 이미지의 size에 **(from ServiceWorker)**가 보인다.

Intermediate

내용이 업데이트 되야하는 페이지나 리소스에 대해서는 동적으로 캐싱 처리를 해야한다.

Dynamic caching

sw.js
const DYNAMIC_CACHE_NAME = "다이나믹-캐시-스토리지1";

// fetch event는 어딘가에서 리소스를 가져올 때 모두 실행된다.
// js를 가져오거나 이미지를 가져오거나 페이지를 가져오거나 등등
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// 캐시에 있으면 repsonse를 그대로 돌려준다.
if (response) {
return response;
}

// 여기서 request를 복사해준다.
// request는 스트림으로 fetch 당 한 번만 사용해야하기 때문이다.
// 근데 event.request로 받아도 실행은 된다
const fetchRequest = event.request.clone();

// if (response) return response 구문을 하나로 합칠 수도 있다.
// return response || fetch(fetchRequest)
return fetch(fetchRequest).then((response) => {
// 응답이 제대로 왔는지 체크한다.
// 구글 문서에는 다음과 같이 처리하라고 되어있는데
// 이 경우 Cross Site Request에 대해 캐싱 처리를 할 수가 없다.
// if(!response || response.status !== 200 || response.type !== 'basic') {
if (!response) {
return response;
}

// 응답은 꼭 복사 해줘야한다.
const responseToCache = response.clone();

// 캐시 스토리지를 열고 정말 캐싱을 해준다.
caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});

// 여기서 response를 내보내줘야 캐싱 처리 후에 리소스를 반환한다.
return response;
});
}),
);
});

오래된 캐시 삭제

캐시 스토리지명을 바꿔 캐시의 버전을 올리면 기존 캐시 스토리지는 삭제해줘야한다.

sw.js
// 서비스 워커가 활성화 될 때
self.addEventListener("activate", (event) => {
// 영구적으로 가져갈 캐시 스트리지 화이트리스트
const cacheWhiteList = [PRE_CACHE_NAME, DYNAMIC_CACHE_NAME];

event.waitUntil(
// 캐시 스토리지의 모든 스토리지명을 가져온다.
caches.keys().then((cacheNames) => {
// 캐시를 삭제하는 건 Promise를 반환하므로 Promise.all을 사용해 끝날 시점을 잡아야한다.
return Promise.all(
// 이 결과는 [Promise, Promise...] 형태가 되시겠다.
cacheNames.map((cacheName) => {
// 각각의 캐시 스토리지명이 화이트 리스트와 같지 않을 경우
if (cacheWhitelist.indexOf(cacheName) === -1) {
// 캐시를 삭제하는 Promise를 배열에 추가한다.
return caches.delete(cacheName);
}
}),
);
}),
);

// activate 시에는 clients claim 메소드를 호출해서
// 브라우저에 대한 제어권을 가져와야한다.
return self.clients.claim();
});

결과적으로 DYNAMIC_CACHE_NAME이 아닌 캐시 스토리지는 삭제된다.

offline fallback

캐시된 페이지와 리소스는 오프라인에서도 접근이 가능하다. 여기서 오류가 발생하면 offline.html 같은 페이지로 떨어지게 할 수 있다. (마치 404 오류 페이지처럼)

sw.js
self.addEventListener("fetch", (event) => {
event.respondWith(
caches
.match(event.request)
.then((response) => {
// ...
})
.catch((error) => {
// 에러 발생시 캐시되어있는 offline.html로 이동시킨다.
return caches.open(CACHE_NAME).then((cache) => {
// 들어온 요청의 Accept 헤더가 text/html 을 포함하고 있다면 (페이지 요청이라면)
if (event.request.headers.get("accept").includes("text/html")) {
// 캐시된 offline fallback 페이지를 보여준다.
return cache.match("/offline.html");
}
});
}),
);
});

Advanced

서비스워커에 static, dynamic cache를 첨가했는데 소스가 너무 지저분하다. 직관적으로 만들어보자.

리팩토링

sw.js
(() => {
const STATIC_CACHE_NAME = "STATIC_CACHE_VERSION_1";
const DYNAMIC_CACHE_NAME = "DYNAMIC_CACHE_VERSION_1";

const WEB_CACHE = {
init() {
self.addEventListener("install", this.staticCacheStrategy.bind(this));
self.addEventListener("activate", this.deleteOldCache.bind(this));
self.addEventListener("fetch", this.dynamicCacheStrategy.bind(this));
},

staticCacheStrategy(event) {
// 스태틱 캐싱
},

deleteOldCache(event) {
// 캐시 삭제
},

dynamicCacheStrategy(event) {
// 다이나믹 캐싱
},
};

WEB_CACHE.init();
})();

IIFE를 사용해 좀 더 예쁘게 변했다.

cross domain request

서비스워커에서 fetch로 외부 리소스를 가지고오면 opaque response가 반환된다. cors 정책이 설정되어 있지 않아 아무 정보도 가지고 올 수 없는 건데, 이런 리소스만 골라서 캐싱처리를 하고 싶다면 request url이나 response content-type을 가지고 처리할 수 있다.

sw.js
const dynamicCacheStrategy = (event) => {
// 캐싱 처리하고 싶은 content-type
const cacheContentsTypes = [
"image/png",
"image/gif",
"image/jpeg",
"application/font-woff",
];

event.respondWith(
caches.match(event.request).then((response) => {
const fetchRequest = event.request.clone();

return (
response ||
fetch(fetchRequest)
.then((response) => {
if (!response) {
return response;
}

// response header에서 content-type을 가져와 비교한다.
// 아니면 request.url이 캐싱처리를 할 외부 url인지 확인한다.
if (
cacheContentsTypes.indexOf(
response.headers.get("content-type"),
) !== -1 ||
event.request.url.indexOf("external.url") !== -1
) {
caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone());
});
}

return response;
})
.catch((error) => console.log(error))
);
}),
);
};

WorkBox

이제 클라이언트 캐시가 어떻게 돌아가는지 확인했으니 구글에서 만든 멋진 라이브러리를 사용해보자.

간결하고 직관적인 문법으로 위의 구문들을 예쁘게 만들 수 있다.

Stratergy

그 전에 캐싱 전략을 알아야한다.

cacheFirst

image from hexo

캐시부터 요청하고 네트워크를 접근해 리소스를 보여준다.

오프라인을 우선적으로 보여주는 페이지에 적합하다.

cacheOnly

image from hexo

캐시에서만 가져온다.

static file들이 여기에 해당된다.

networkFirst

image from hexo

네트워크를 먼저 접근하고 오프라인일 경우 캐시를 가져온다. 이 방법은 연결이 원활하지 않거나 느린 경우 네트워크가 실패할 때까지 기다린 뒤 리소스가 보여지므로 UX에 좋지 않을 수 있다.

networkOnly

image from hexo

캐시가 필요없는 GET 메소드가 아닌 다른 메소드가 주로 여기에 해당된다.

staleWhileRevalidate

image from hexo

이 방식은 그림을 보고 이해하는게 빠르다. 캐시를 먼저 가져오고 다음 요청은 네트워크에서 요청된 리소스의 캐시를 반환한다.

주로 이 방식이 사용된다.

Cache then network

image from hexo

페이지가 두 개의 요청(캐시에 요청, 네트워크에 요청)을 동시에 하고 캐시된 데이터를 먼저 표시한 다음 네트워크 데이터가 도착하면 페이지를 업데이트를 한다. WorkBox에는 없는 strategy인데 networkFirst보다 UX상 좋다고 한다.

설치

ServiceWorker에서는 라우팅 기능을 사용하기 위해 구글 CDN에서 제공하는 스크립트를 사용하고 Pre-Cache를 정의하기 위해 로컬에서 workbox-cli를 추가해야한다.

importScript

importScript는 ServiceWorker 파일에서만 사용가능한 구문으로 importrequire와 같다고 보면 된다.

importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/3.2.0/workbox-sw.js",
);

workbox-cli

yarn global add workbox-cli

메소드

precache

프리캐시는 정적 자원들을 미리 캐싱처리해서 Cache Only Stratergy를 사용하는 것이다. 주로 이미지나 css, vendor-js 등을 여기에 담아준다.

workbox-cli를 설치하고 wizard 명령어를 실행하면 config 파일이 생성된다.

workbox wizard

설정을 입맛에 맞게 변경해주고 pre-cache할 파일 목록을 생성해주는 명령어를 실행하자

workbox injectManifest config.js

ServiceWorker에 workbox.precaching.precacheAndRoute([]) 구문이 있다면 목록이 들어가 있는 걸 확인할 수 있다.

workbox.precaching.precacheAndRoute([
{
url: "css/fonts/fontawesome/fontawesome-webfont.eot",
revision: "674f50d287a8c48dc19ba404d20fe713",
},
{
//...
},
]);

Routing

WorkBox를 사용하는 이유는 바로 이 라우팅에 있다. 위에서 많은 양의 라우팅 분기 코드로 캐싱을 처리했는데 WorkBox는 다음과 같이 간결해진다. 아래는 이 블로그에 사용하고 있는 코드이다.

importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js",
);

// importScripts 후 타이밍 차이로 인해 모듈을 못 불러오는 경우를 방지하기 위해
// 캐싱 정책 모듈 로드를 기다린다.
workbox.loadModule("workbox-strategies");

workbox.skipWaiting();
workbox.clientsClaim();

// accept 헤더에 text/html 값이 있으면 (html 페이지 요청일 경우)
// networkFirst 캐싱
workbox.routing.registerRoute((routeData) => {
return routeData.event.request.headers.get("accept").includes("text/html");
}, workbox.strategies.networkFirst());

// imgur 요청일 경우 cacheFirst 캐싱
workbox.routing.registerRoute(
/.*(?:imgur)\.com.*$/,
workbox.strategies.cacheFirst(),
);

// jsdelivr 요청일 경우 stateWhileRevalidate 캐싱
workbox.routing.registerRoute(
/.*(?:jsdelivr)\.net.*$/,
workbox.strategies.staleWhileRevalidate(),
);

// bootcss 요청일 경우 stateWhileRevalidate 캐싱
workbox.routing.registerRoute(
/.*(?:bootcss)\.com.*$/,
workbox.strategies.staleWhileRevalidate(),
);

코드가 너무 예뻐졌다. callback으로 Request의 모든 걸 캐치해 낼 수 있다. 더 구체적으로 쓰고 싶으면 문서를 참조하자

부가 기능

이 기능 말고도 다음과 같은 멋진 기능을 사용할 수 있다, 하지만 언제 쯤 써볼 수 있을지..

offline GA

오프라인에서도 Google Analytics 를 사용할 수 있다.

한 줄의 코드만 삽입해주면 된다.

workbox.googleAnalytics.initialize();

Webpack

웹팩에 WorkBox-Webpack-Plugin을 붙힐 수 있는데 아직 도전을 안 해봤다. 후술할 sw-precache를 굳이 뺄 이유가 없고, 레퍼런스도 워낙 많기에..

sw-precache

create-react-app이나 Vue-Cli@angular/cli 모두 클라이언트 캐싱에 WorkBox 대신 이 라이브러리를 사용하고 있다. (WorkBox에서 캐싱기능만 떼어낸 라이브러리라고 보면 된다)

기본 설정을 굳이 안 건들여도 되고, exact해서 사용 중이라면 옵션을 라이브러리를 한 번 쯤 봐주는 것도 나쁘지 않다.

참고

여담

  • 기술 정리하는 건 시간이 너무 오래 걸린다.
  • Redis, eTag, Vanish, SW 까지 사용하면 성능상에 이점은 있겠지만 키를 어떻게 관리해야될지가 중요할 듯