0%

211210

마이크로프론트앤드

  • RunTime Integration
    • 개발 마친 앱을 url 나 다른 포트를 통해 전달하는 등의 방식
    • 장점: 완전 독립적이다.
    • 단점: webpack 설정 등.. 이해하기 어려운 부분이 있다.
    • → 이번 과정에서는 RunTime Integration에 대해서 공부한다.
  • 구조를 어떻게 ? 작은 앱을 만든다고 생각한다. 예를들어서 prodicts와 cart 앱을 만들어서 각각 다른 포트로 연다고 생각하면 된다 이를 위해서 각각의 폴터에 package.json, html, 을 만들고 webpack 을 이용해서 파일을 bundle하고 서버를 연다.

211209

백엔드

  • 201 데이터가 잘 만들어졌습니다 상태 코드
  • 204 정상적으로 삭제 했고 이제 리소스가 없음. sendStatus(204) → 상태 메세지만 반환함.
  • postman 에서 RUN 버튼 누르면 컬랙션 안에 모든 API를 테스트할 수 있다.
  • mvc 패턴 model, view, controller 를 분리하는 패턴 model (DB), controller(logic), View(ui) → 서버에서 View 는 router 와 밀접한 관련이 있다.
  • typescript 에서 req.query 에 타입을 정의하고 싶을때 req: Request<{}, {}, {}, Type> 으로 정의할 수 있다.
  • 마찬가지로 body 나 params 의 타입을 설정하고 싶다면 Request 에 generic 으로 순서에 맞게 넣어주면 된다.

마이크로 프론트엔드

  • 각각의 페이지를 각각의 SPA로 만든다. 서로 커뮤니케이션을 하는 것이 아니라 API를 통해 커뮤니케이션 하게 만든다.
  • 장점? 완전히 독립적인 앱 두게를 갖게 된다. 만약 각각의 팀이 개발하고 있다면? 각각의 팀이 독립적인 결정을 해도 영향을 주지 않는다!
  • 예를들어 팀 1에서는 react로 구현하고 팀2 에서는 vue 나 angular 같은걸로 개발할 수 있게 된다.
  • 한 페이지 내에서도 가능하다. 근데 각각의 앱을 어디에 표시할지 어떻게 알까? 컨테이너라는 앱도 만들어서 어떻게 표시할지 구현할 수 있다. 즉 2개의 마이크로프론트 앱이 있다면 한개의 컨테이너 앱을 더 두어서 어떻게 배치할지 정하는 것이다.
  • 컨테이너를 어떻게 구현할수 있을지는 크게 3가지 관점이 있다.
    1. build time integration → 브라우저에 컨테이너가 로딩 되기 전에 각각의 앱에 접근할 수 있다.
    2. run time integration → 브라우저에 컨테이너가 로딩 된 이후에 각각의 앱에 접근할 수 있다.
    3. server integration → backend 에서 해야할 내용이므로 여기서는 다루지 않는다.
  • build tim integration에 가장 간단한 방법?
    • 작업 완료하면 npm 패키지로 배포한다. 다음 컨테이너에서 받아서 사용한다.
    • 장점 : 쉽다. 이해하기 쉬움.
    • 단점 : container 가 redepoly 되면 패키지도 다시 update 됨. 컨테이너와 강하게 결합되게 된다. 마이크로론트엔드에 컨셉에 맞지 않음.

알고리즘

  • 실제로 재귀 함수로 DFS 구현하는 경우가 있다.
  • BFS 는 보통 큐라서 만약 둘다 가능하다면 BFS로 푸는게 나은 경우가 많다.
  • 100만 → o(N) 만큼 필요함

211207

알고리즘

이진탐색 템플릿

1
2
3
4
5
6
7
const binarySearch(arr, target, start, end) {
if (start > end) return -1;
let mid = parseInt((start + end) / 2);
if (arr[mid] == target) return mid
else if (arr[mid] > target) return binarySearch(arr, target, start, mid -1);
else return binarySearch(arr, target, mid + 1, end);
}

반복문으로 왠만하면 반복문이 빠름

1
2
3
4
5
6
7
8
function binarySearch(arr, tareget, start, end) {
while (start <= end) {
let mid = parseInt((start + end) / 2);
if (arr[mid] == target) return mid;
else if (arr[mid] > target) end = mid - 1;
else start = mid + 1;
}
}

lowerBound(a, x) 하한선 → 정렬된 데이터가 있을때 정렬을 유지하면서 배열 a 에 x를 가장 왼쪽에 넣을 인덱스 반환

upperBound(a, x) 상한선 → 정렬 유지하면서 배열 a 에 가장 오른쪽에 넣을 인덱스 반환

lowerBound:

1
2
3
4
5
6
7
8
function lowerBound(arr, target, start, end) {
while(start < end) {
let mid = parseInt((start + end / 2);
if (arr[mid] >= target) end = mid;
else start = mid + 1;
}
return end
}

uppserBound :

1
2
3
4
5
6
7
8
function lowerBound(arr, target, start, end) {
while(start < end) {
let mid = parseInt((start + end / 2);
if (arr[mid] > target) end = mid;
else start = mid + 1;
}
return end
}

countByRange : 같이 사용하는 것

1
2
3
4
5
function countByRange(arr, leftValue, rightValue) {
let rightIndex = upperBound(arr, rightValue, 0, arr.length);
let leftIndex = lowerBound(arr, leftValue, 0, arr.length);
return rightIndex - leftIndex;
}

보통 3가지중 하나

  1. 단순 이진 탐색
  2. countByRange
  3. 파라메트릭 서치 → 이진탐색이 아닌거 같지만 이진탐색인걸 찾아야 함. (예 혹은 아니오로 )

211208

백엔드

  • 404 api 없음
  • 500 서버 에러
  • ⇒ 의미가 충분한 status code가 있다면 굳이 json을 보내주지 않아도 괜찮다.
  • router를 나눌것.

프론트엔드

  • input에서 제어 컴포넌트로 만들면 계속 해서 함수형 컴포넌트가 호출 되어서 컴포넌트 안에 debounce 가 제대로 작동하지 않는 이슈가 있었음.
  • 추천 검색어를 선택하면 input 에 value 가 바뀌어야 하는데 state로 관리 하지 못하면 문제가 생김.. 그렇다고 제어 컴포넌트로 만들면 위에 같은 문제..
  • 예를 들어 asdf 입력하면 asdf * 3번에 요청이 서버로 가는 이슈.. 원래는 asdf → asdf 로 한번에 요청하는 방식 → dom 을 조작해보자 생각함.
  • useRef 로 dom 을 다룰수 있을줄 알았는데, 그냥 ref 로 넘겨주는것으로 dom처럼 사용할 수는 없음..
  • useRef.current 를 사용해서 documnet.querySelector() 로 componentDidMound 시점에 넘겨주면 실제 Dom 을 조작하듯이 사용할수 있음.
  • 근데 생각해보니 useEffect 로 input에 변화로 debounce 동작하게 하면 문제 될거 없어보임.. 이렇게 삽질을 합니다

리액트 디자인 패턴

  • 디자인 패턴이 필요한 이유 ? 모두가 사용하기 쉬운 API를 재공하고, 재 사용하 위해서, 확장성을 위해서 → 모두가 알고 있는 약속을 통해서
  • 검증된 패턴을 통해서 낮은 결합도와 재사용성 높은 컴포넌트를 만들 수 있음.

211202

DOM

  • document.elementFromPoint 를 사용해 포인터 아래의 드롭할 수 있는 요소를 감지한다.
  • Pointer Event 로 touch를 다룰때는 css 속성에 touch-event: none 을 주어야 한다.
  • multi-touch surport pointerId isPrimary
  • pressure , width/height
  • cut, copy, paste 같은 이벤트는 event.clipboardData 를 통해서 클립보드에 저장된 데이터를 확인할 수 있다.

리액트 타입스크립트

  • 함수의 실행 결과를 타입으로 가져오고 싶을때는 ReturnType<typeof increase> (increase 는 함수) 와 가은 식으로 사용할 수 있다.
  • typesafe-actions 를 사용하면 리덕스 관련한 작업을 보다 간단하게 할 수 있다.

알고리즘

항상 특정한 조건에 따라서 다른 상황으로 바꿔주기 그리디는 정당성을 확인하는것이 중요하다. 먼저 정렬을 하고 정당성을 확인해보는것도 좋은 방법 N이 200000 이고 1초 → 정렬 가능, 그리디 뭐가 있지?

211201

리액트 타입스크립트 사용

  • fucntion 키워드를 사용하는 typescirpt 컴포넌트는 children 을 가지지 않는다. (React.FC 를 명시하지 않을 경우) React.FC 로 선언한 컴포넌트는 children을 가진다.
  • children 의 타입은 React.ReactNode
  • styled Components를 사용할때나 여러가지 상황에서 굳이 React.FC를 선언하면 불편해진다. 명확하게 표시하고 싶을때 사용해도 되지만 굳이 사용하지 않아도 무방하다.
  • useReducer 를 사용할땐 Action 타입을 지정해 준다.

알고리즘

  • 그리드 알고리즘 : 매순간 최적이라고 생각하는 것을 선택한다. 하지만 반드시 답을 보장하지는 않는다.

수업

npm i express-generator ( express 탬플릿을 만들어줌)

styled components

  • theme 를 관리하기 위해서 styled compoents 에 ThemeProvider, ThemContext 조합을 사용한다. 이렇게 theme 을 주입하면 모든 컴포넌트에서 props로 theme 변수에 접근할 수 있다.
  • useContext 를 사용해 인수로 ThemeContext 를 넘겨주면 컴포넌트 내에서 변수로 사용할 수 도 있다. 여기서 테마를 바꾸는 setTheme 같은 작업을 수행할 수 있다.
  • 테마 같은 경우는 theme 폴더에 파일별로 관리하되 id 를 주어서 어떤 테마인지 식별할 수 있도록 하면 현제 내 테마 상태가 어떤건지 id로 쉽게 판별할 수 있다.

backend express

  • app.use(express.urlencoded({extended:false})) ⇒ html form → body 로 변환 해 주는 것
  • cors: 클라이언트와 서버가 다른 도메인에 있다면 원칙적으로는 어떤 데이터도 받을 수 없다. 서버에서 Access-Contorol-Allow-Origin 설정 되어 있어야 함. Access-Contorol-Allow-Methods 로 가능한 메서드 설정해 주어야 함. 이럴 때 사용하는 미들웨어 cors → app.use(cors( { origin: [‘http~’]})) , option으로 origin, optionSuccessStatus, credentials → Access-Contorols-Allow-Credintial 설정
  • 그 밖에 자주 사용되는 미들웨어 : cookie-parser (req.cookie 로 읽을 수 있도록 함.), morgan (어떤 요청, 얼마나 걸렸는지 로그를 남기고 싶을때) app.use(morgan(‘combined’)) <compbine → 포맷임, 다른건 깃헙에서 찾아보길>, helmet(보안에 필요한 헤더를 추가해줌)

XMLHttpRequest

브라우저에 내장 객체이다. 자바스크립트로 request를 보낼 수 있게 해준다. 이름과 달리 XML 뿐만 아니라 다른 어떤 형식에 대해서도 가능하다.

요즘은 fetch라는 모던한 기능이 있어서 더이상 사용되지 않는다. 모던 웹에서 3가지 이류로 사용된다.

  1. 예전 코드에 이미 적용된 경우
  2. 플로필 하고 싶지 않고 옛날 브라우저에서도 가능하게 하려면 사용해야 함.
  3. 업로드 과정을 트래킹 한다든지 아직 fetch에서 지원하지 않는 기능을 사용하려면

the basic

XMLHttpRequest에는 비동기와 동기 동작이 있다. 주로 사용되는 동기 동작부터 알아본다.

  1. XMLHttpRequest 생성
1
let xhr = new XMLHttpRequest();
  1. 보통 생성하고 나서 초기화 한다.
1
xhr.open(mehtod, URL, [async, user, password]);
  • method : HTTP method 보통 GET이나 POST이다.
  • URL : 리퀘스트 보내는 URL, 스트링이나 URL 오브젝트가 될 수 있다.
  • async : 만약 false로 설정할 경우 동기로 동작한다.
  • user, password : 기본 HTTP에서 인증을 위해 사용함. 필요할때 추가한다.

open이라는 메서드는 request를 구성한다고 생각하면 된다. 실제적으로 요청을 보내는 것은 send라는 메서드를 통해서 진행된다.

  1. 보내기
1
xhr.send([body]);

실제 적으로 연결을 open 하고 서버로 데이터를 request 한다. body 파라미터를 통해서 데이터를 보낼 수 있는데, GET메서드의 경우 body 파라미터가 필요 없을 수 있다. POST 메서드로 요청을 보낼때 body에 데이터를 담아서 보낸다.

  1. 응답에 대한 xhr 이벤트를 듣는다.

다음에 세가지 이벤트가 주로 폭넓게 사용된다.

  • load : request가 완전히 완료 되었을때(심지어 400 이나 500에 상태코드를 받았을 때도), 응답이 완전히 받아졌을때도
  • error : request가 만들어지지 않았을때, 잘못된 URL이거나 하는 등..
  • progress : 얼마나 다운로드 됬는지 리포트 한기 위해서 주기적으로 트리거 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xhr.onload = function () {
alert(`Loaded: ${xhr.status} ${xhr.response}`);
};

xhr.onerror = function () {
// only triggers if the request couldn't be made at all
alert(`Network Error`);
};

xhr.onprogress = function (event) {
// triggers periodically
// event.loaded - how many bytes downloaded
// event.lengthComputable = true if the server sent Content-Length header
// event.total - total number of bytes (if lengthComputable)
alert(`Received ${event.loaded} of ${event.total}`);
};

progress 를 프린트하는 전체 예제는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1. Create a new XMLHttpRequest object
let xhr = new XMLHttpRequest();

// 2. Configure it: GET-request for the URL /article/.../load
xhr.open("GET", "/article/xmlhttprequest/example/load");

// 3. Send the request over the network
xhr.send();

// 4. This will be called after the response is received
xhr.onload = function () {
if (xhr.status != 200) {
// analyze HTTP status of the response
alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found
} else {
// show the result
alert(`Done, got ${xhr.response.length} bytes`); // response is the server
}
};

xhr.onprogress = function (event) {
if (event.lengthComputable) {
alert(`Received ${event.loaded} of ${event.total} bytes`);
} else {
alert(`Received ${event.loaded} bytes`); // no Content-Length
}
};

xhr.onerror = function () {
alert("Request failed");
};

서버가 응답하면 다음에 xhr 속성에서 결과를 받아올 수 있다.

status : HTTP status code를 확인할 수 있다. HTTP가 아닌 오류의 경우에는 0이 될 수 있다.

statusText : HTTP status message를 확인할 수 있다. (OK, Not Found 같은거..)

response(오래된 스크립트는 responseText를 사용할 것이다.) : 서버에 응답 본문이다.

또한, timeout 프로퍼티를 설정해서 설정한 시간동안 응답을 받이 못하면 timeout 이벤트가 트리거 되게 할 수 도 있다.

1
xhr.timeout = 10000; // timeout in ms, 10 seconds

파라미터를 설정하고 적절히 인코딩 되는것을 보장하기 위해서 URL Object를 사용할 수 도 있다.

1
2
3
4
5
let url = new URL("https://google.com/search");
url.searchParams.set("q", "test me!");

// the parameter 'q' is encoded
xhr.open("GET", url); // https://google.com/search?q=test+me%21

Response Type

xhr.responseType 프로퍼티를 사용하엿 응답 포멧을 설정할수 있다.

  • "" : string
  • "text" : string
  • "arraybuffer" : ArrayBuffer
  • "blob" - Blob
  • "document" - XML document
  • "json" - JSON

JSON으로 응답 받는 예제는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let xhr = new XMLHttpRequest();

xhr.open("GET", "/article/xmlhttprequest/example/json");

xhr.responseType = "json";

xhr.send();

// the response is {"message": "Hello, world!"}
xhr.onload = function () {
let responseObj = xhr.response;
alert(responseObj.message); // Hello, world!
};

예전에는 xhr.responseTextxhr.responseXML 같은걸 사용했는데 요즘에는 xhr.responseeType을 설정하고 xhr.response를 받는 방법을 주로 사용한다.

Ready states

XMLHttpRequest는 진행됨에 따라서 상태가 바뀐다. 접근 가능한 상태는 다음과 같다.

1
2
3
4
5
UNSENT = 0; // initial state
OPENED = 1; // open called
HEADERS_RECEIVED = 2; // response headers received
LOADING = 3; // response is loading (a data packed is received)
DONE = 4; // request complete

순서는 다음과 같아 진다. 0 -> 1 -> 2 -> 3 -> … -> 3 -> 4, 3이 반복되는 이유는 네트워크에서 패킷을 받을때마다 3 상태가 되기 때문이다.

readystateChange이벤트를 통해 트래킹 할 수 있다.

1
2
3
4
5
6
7
8
xhr.onreadystatechange = function () {
if (xhr.readyState == 3) {
// loading
}
if (xhr.readyState == 4) {
// request finished
}
};

이 이벤트는 예전에 사용하던 것으로 역사적인 이유로 남아있다. 요즘은 open/ error / progress 같은것을 사용하면 된다.

Aborting request

xhr.abort() 를 호출하면 즉시 request를 중단할 수 있다.

1
xhr.abort();

abort가 트리거 되면. xhr.status 는 0 이 된다.

Synchronous requests

open에 3번째 파라미터인 async에 값이 false 로 절정되면 요청이 비동기로 실행될 것이다. 이는 alert 이나 prompt와 같이 실행이 멈춘되 응답을 받으면 다시 실행된다는 것을 의미한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let xhr = new XMLHttpRequest();

xhr.open("GET", "/article/xmlhttprequest/hello.txt", false);

try {
xhr.send();
if (xhr.status != 200) {
alert(`Error ${xhr.status}: ${xhr.statusText}`);
} else {
alert(xhr.response);
}
} catch (err) {
// instead of onerror
alert("Request failed");
}

동기를 사용하면 사이트가 멈춘것 처럼 보이기 때문에 거의 사욯아지 않는다. 또한 진행 사항을 보는 기능이던지 timout을 설정하는 기능이던지 XMLHttpRequest에 정의되어 있는 기능을 사용할 수 없게 된다. 따라서 대부분의 경우는 비동기를 사용하면 된다.

HTTP-headers

XMLHttpRequests에 요청에 헤더를 설정하고 헤더를 읽을 수 도 있다.

setRequestHeader(name, value)

리퀘스트에 헤더를 설정한다.

1
xhr.setRequestHeqader("Content-Type", "application/json");

헤더 제한 : 몇몇에 헤더들은 브라우저에 의해서 관리된다. (Referer and Host)
헤더를 제거할 수는 없다. 헤더는 추가 설정되지 덮어씌워지지 않는다.

1
2
3
4
5
xhr.setRequestHeader("X-Auth", "123");
xhr.setRequestHeader("X-Auth", "456");

// the header will be:
// X-Auth: 123, 456

getResponseHeader(name)

header를 주어진 이름을 통해서 얻는다. (Set-CookieSet-Cookie2 제외)

1
xhr.getReponseHeader("Content-Type");

getAllResponseHeaders()

Set-CookieSet-Cookie2를 제외한 모든 헤더를 얻는다.

1
2
3
4
Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT

개행은 항상 \r\n이다. os에 의존하지 않는다.

헤더 덮어씌우기 예제

1
2
3
4
5
6
7
8
9
10
let headers = xhr
.getAllResponseHeaders()
.split("\r\n")
.reduce((result, current) => {
let [name, value] = current.split(": ");
result[name] = value;
return result;
}, {});

// headers['Content-Type'] = 'image/png'

POST, FormData

POST로 리퀘스트를 보낼때 FormData Object를 사요할 수 있다.

1
2
let formData = new FormData([form]); // create an object
formdata.append(name, value); // appends a field

form을 설정하면 xhr.open('POST', ...) POST 메서드로 리퀘스트를 설정하고, xhr.send(formData) 에 send 메서드에 formData를 전달해서 리퀘스트 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<form name="person">
<input name="name" value="John" />
<input name="surname" value="Smith" />
</form>

<script>
// pre-fill FormData from the form
let formData = new FormData(document.forms.person);

// add one more field
formData.append("middle", "Lee");

// send it out
let xhr = new XMLHttpRequest();
xhr.open("POST", "/article/xmlhttprequest/post/user");
xhr.send(formData);

xhr.onload = () => alert(xhr.response);
</script>

form 은 기본적으로 multipart/form-data로 보내진다. 만약 json으로 보내고 싶으면 JSON.stringfy() 를 통해 스트링으로 변환 후 보내면 된다. 이때, 헤더에 Content-Type: applicatioin/json을 설정해 두면 서버 측에 프레임워크에서 자동으로 JSON으로 디코딩 할수 있는 정보를 줄수 있다.

1
2
3
4
5
6
7
8
9
10
11
let xhr = new XMLHttpRequest();

let json = JSON.stringify({
name: "John",
surname: "Smith",
});

xhr.open("POST", "/submit");
xhr.setRequestHeader("Content-type", "application/json; charset=utf-8");

xhr.send(json);

sned(body) 에서 body에는 정말로 다양한 매개변수를 받을 수 있다. 사용 가능한 예로는 body, Blob 그리고 BufferSource 객체가 있다.

Upload progress

progress 이벤트는 다운로드 할때만 동작한다. post 요청이 보내지고 업로드가 완료되고 나면 response가 오는데 이때 추적을 할 수 있다.

만약 Upload에 트랙킹을 사용하고 싶다면 이때 사용하는 다운로드를 트래킹할때 사용하는 xhr.onprogress는 도움이 되지 못할 것이다. 대신에 xhr에서는 명시적으로 업로드를 트래킹할 수 있는 xhr.upload를 제공한다.

다음에 이벤트를 발생시킨다.

  • loadstart - upload started
  • progress - triggers periodically during the upload
  • abort - upload aborted.
  • error - non-HTTP error.
  • load - upload finished successfully.
  • timeout - upload timed out (if timout property is set.)
  • loadend = upload finished with either success of error.

헨들러 예제 :

1
2
3
4
5
6
7
8
9
10
11
xhr.upload.onprogress = function (event) {
alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
};

xhr.upload.onload = function () {
alert(`Upload finished successfully.`);
};

xhr.upload.onerror = function () {
alert(`Error during the upload: ${xhr.status}`);
};

실전 예제 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<input type="file" onchange="upload(this.files[0])" />

<script>
function upload(file) {
let xhr = new XMLHttpRequest();

// track upload progress
xhr.upload.onprogress = function (event) {
console.log(`Uploaded ${event.loaded} of ${event.total}`);
};

// track completion: both successful or not
xhr.onloadend = function () {
if (xhr.status == 200) {
console.log("success");
} else {
console.log("error " + this.status);
}
};

xhr.open("POST", "/article/xmlhttprequest/post/upload");
xhr.send(file);
}
</script>

Cross-origin requests

기본적으로 fetch의 정책와 같이 Cross-origin request 를 보낼 수 있다. fetch 와 바찬가지로 cookie와 HTTP-authorization을 다른 origin으로 보낼 수 없는데 이를 이런 설정을 사용하기 위해서는 xhr.withCredentialstrue가 되어야 한다.

1
2
3
4
5
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;

xhr.open('POST', 'http://anywhere.com/request');
...

파일 업로드 재개하기

fetch 메서드를 사용하면 파일을 업로드 할 수 있다. 용량이 큰파일을 업로드할 때 나누어서 업로드 해야하는 경우가 생기는데, 이때 사용하는 내장된 기능은 없지만 부분적으로 구현할 수 있는 여러 기능은 있다.

해당 기능을 사용하기 위해서는 업로드 진행률을 알아야 하는데 fetch 로는 업로드 진행률을 알지 못하기 때문에 XMLHttpRequest를 사용해서 알아낸다.

별 도움 안되는 진행률 이벤트

앞서서 xhr.upload.onprogress로 업로드 진행률을 알수 있다는 것을 살펴 보았다. 하지만 이는 브라우저에서 요청 보낼때 작동할 뿐이지, 서버에서 데이터를 받았을때 작동하지는 않는다.

이는 네트워크 문제나 여러가지 상황에서 서버가 리시브 받지 못했음에도 업로드 한것처럼 보일수 있는 위험이 있다. 따라서 서버에서 정확히 얼마만큼 리시브 받았는지에 대한 정보가 필요한데 이는 따로 요청을 보내서 확인해야 한다.

알고리즘

  1. 업로드할 파일에 고유값을 생성한다.
1
let fileId = file.name + "-" + file.size + "-" + file.lastModifiedDate;

파일 아이디는 파일 업로드를 재개할 때 서버에 어떤 파일을 재개할지 말해주는데 필요하다.

  1. 서버에 요청을 보내어 얼마만큼 바이트를 전송했는지 질의한다.
1
2
3
4
5
6
7
8
let response = await fetch("status", {
headers: {
"X-File-Id": fileId,
},
});

// 서버가 얼마만큼 파일 바이트를 가졌는지 확인한다.
let startByte = +(await response.text());

서버가 X-File-Id 헤더에서 파일 업로드를 추적한다고 가정한다. 이 작업은 사버사이드에서 구현되어 있어야 하고, 아직 파일이 서버에 없으면 서버는 0으로 응답해야 한다.

  1. startByte 에서 파일을 보내기 위해 Blobslice 메서드를 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xhr.open("POST", "upload", true);

// 파일 아이디를 통해 서버는 어떤 파일을 업로드 받을지 알게 된다.
xhr.setRequestHeader("X-File-Id", fileId);

// 서버는 업로드를 제개할 파일의 시작 바이트를 통해 파일 업로드가 재개될 것을 알게 된다.
xhr.setRequestHeader("X-Start-Byte", startByte);

xhr.upload.onprogress = (e) => {
console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`);
};

// 업로드를 할 파일은 input.files[0]나 또 다른 출처가 될 수 있습니다.
xhr.send(file.slice(startByte));

파일 아이디인 X-File-Id를 서버로 보내 업로드를 진행할 파일이 어떤 것인지 알리고, 시작 바이트인 X-Start-Byte를 서버에 보내 파일 업로드를 초기화하지 않고 파일 업로드를 다시 시작한다는 것을 서버에 알게 한다.

Long polling

Long polling 은 WebSocket 또는 Server Side Event와 같은 특정 프로토콜을 사용하지 않고 서버와 지속적으로 연결하는 가장 간단한 방법이다.

Reqular Polling

서버에서 새로운 정보를 얻는 가장 간단한 방법은 주기적인 요청이다. 예를 들어 새로운 정보를 얻기 위해 10초 간격으로 서버에 “새로운 정보 있나요?” 요청을 보내는 것과 같다.

서버는 이에대한 응답으로 클라이언트를 확인하고 패킷을 보낸다. 정상적으로 작동하지만 다음과 같은 문제가 있다.

  1. 메시지는 최대 10초 지연으로 전달 된다.
  2. 서버는 사용자의 유무나 메세지가 없더라도 계속해서 요청을 받는다. 이는 성능 부하를 일으킨다.

개선해 보자.

Long polling

Long polling은 구현하기도 쉽고 지연 없이 메세지를 전달한다.

  1. 요청이 서버로 전송된다.
  2. 서버는 보낼 메세지가 있을 때까지 연결을 닫지 않는다.
  3. 메세지가 나타나면 서버는 요청에 응답한다.
  4. 브라우저는 즉시 새 요청을 만든다.

보통 요청은 보내지면 바로 응답을 받는다. 그런데 Long polling은 서버가 요청을 받고 대기하고 있다고 전달할 메세지가 있다는 이벤트가 발생하면 그때 응답한다. 따라서 연결을 유지할 수 있게 되는 것이다.

네트워크 오류로 인해 연결이 끊어지면 브라우저는 즉시 새 요청을 보낸다.

긴 요청을 만드는 클라이언트에 subscribe 함수 예제:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
async function subscribe() {
let response = await fetch("/subscribe");

if (response.status == 502) {
// Status 502 is a connection timeout error,
// may happen when the connection was pending for too long,
// and the remote server or a proxy closed it
// let's reconnect
await subscribe();
} else if (response.status != 200) {
// An error - let's show it
showMessage(response.statusText);
// Reconnect in one second
await new Promise((resolve) => setTimeout(resolve, 1000));
await subscribe();
} else {
// Get and show the message
let message = await response.text();
showMessage(message);
// Call subscribe() again to get the next message
await subscribe();
}
}

subscribe();

요즘은 web push 와 같은 기능을 사용할 수 있기 때문에, 실시간이 중요한 서비스에서는 web push 기능을 사용하는것이 바람직하다.

웹 소켓

웹 소켓을 사용하면 HTTP의 새로운 요청이나 중단 없이 데이터를 주고 받을 수 있다. 커넥션을 종료 시키지 않은 체 패킷 형태로 이루어지며 양방향 통신이 가능하다. 실시간으로 데이터를 교환하는 시스템이나 주식 트레이딩 시스템에 적합하다.

간단한 예시

1
let socket = new WebSocket("ws://javascript.info");

ws말고 wss:// 라는 프로토콜도 사용하는데 이는 HTTPHTTPS의 관계와 유사하다.

항상 wss://를 사용해라.
ws를 사용하면 데이터가 암호화 되지 않고 전달되는데 오래된 프락시 서버에 경우는 웹솟켓을 알지 못해서 이상한 헤더를 붙혀서 전달하는 일이 발생한다. wss를 사용하면 TLS라는 보안 계층을 통해서 데이터가 암호화 되어서 프락시에서 열어 볼 수 없고, 복호화는 받는 쪽에서 연결 하기 때문에 안전하게 연결할 수 있다.

다음 이벤트를 사용할 수 있다.

  1. open - 커넥션이 제대로 만들어졌을때 발생함.
  2. message - 데이터를 수신하였을때 발생함.
  3. error - 에러가 생겼을때 발생함.
  4. close - 커넥션이 종료되었을때 발생함.

커넥션이 만들어진 상태에서 무엇을 보내고 싶다면 socket.send(data)를 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let socket = new WebSocket(
"wss://javascript.info/article/websocket/demo/hello"
);

socket.onopen = function (e) {
alert("[open] 커넥션이 만들어졌습니다.");
alert("데이터를 서버에 전송해봅시다.");
socket.send("My name is Bora");
};

socket.onmessage = function (event) {
alert(`[message] 서버로부터 전송받은 데이터: ${event.data}`);
};

socket.onclose = function (event) {
if (event.wasClean) {
alert(
`[close] 커넥션이 정상적으로 종료되었습니다(code=${event.code} reason=${event.reason})`
);
} else {
// 예시: 프로세스가 죽거나 네트워크에 장애가 있는 경우
// event.code가 1006이 됩니다.
alert("[close] 커넥션이 죽었습니다.");
}
};

socket.onerror = function (error) {
alert(`[error] ${error.message}`);
};

이벤트는 open -> message -> close 순서로 발생한다.

웹소켓 핸드셰이크

new WebSocket(url)를 호출해 소켓을 생성하면 즉시 연결이 시작된다.

브라우저는 서버에게 웹소켓을 지원하는지 묻고 지원한다는 응답이 오면 이제 HTTP가 아닌 WebSocket protocol로 통신한다.

요청 헤더 예시 :

1
2
3
4
5
6
7
GET /chat
Host: javascript.info
Origin: https://javascript.info
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
  • Origin - 클라이언트 오리진을 나타낸다. 서버는 Origin 헤더를 보고 웹사이트와 소켓통신을 할지 결정하기 때문에 Origin 헤더는 웹소켓 통신에 중요한 역할을 한다.
  • Connection: Upgrade - 클라이언트 측에서 프로토콜을 바꾸고 싶다고 알려줌.
  • Upgrade: websocket - 클라이언트 측에서 요청한 프로토콜은 websocket 이라는 것을 의미한다.
  • Sec-WebSocket-Key - 보안을 위해 브라우저에서 생성한 키를 나타낸다.
  • Sec-WebSocket-Vertion - 웹소켓 프로토콜 버전이 명시된다. 예시는 13버전

웹소켓 핸드셰이크는 모방이 불가능하다. (바닐라 자바스크립트로 헤더를 설정하는 건 기본적으로 막혀있다.)

응답 예시 :

1
2
3
4
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=

Sec-WebSocket-AcceptSec-WebSocket-Key와 밀접한 관계가 있다. 브라우저는 이 헤더를 보고 특정한 알고리즘을 사용해 자신이 설정한 key인지 확인한다.

Extensions와 Subprotocols 헤더

기능확장과 서브 프로토콜로 데이터를 전달할때 Sec-WebSocket-ExtenstionsSec-WebSocket-Protocol 헤더를 지원한다.

  • Sec-WebSocket-Extensions : deflate-frame 이 헤더는 데이터 압축을 지원한다는 것을 의미함. 이 헤더는 브라우저에 의해 자동 생성되는데, 그 값엔 데잍터 전송과 관련된 무언가나 웹소켓 프로토콜 기능 확장과 관련된 무언가가 여러개 나열된다.
  • Sec-WebSocket-Protocol: soap, wamp - 이렇게 설정되면 평범한 헤더가 아닌 SOAP, WAMP 프로토콜을 준수하는 데이터를 전송하겠다는 의미이다.

이 헤더는 new WebSocket에 두번째 매겨변수에 값을 넣어서 설정할 수 있다.

1
let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]);

이때 서버는 지원 가능한 익스텐션과 프로토콜을 응답 헤더에 담아 클라이언트에 전달해야 한다.

예시 :
클라이언트 :

1
2
3
4
5
6
7
8
9
GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascript.info
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp

서버 :

1
2
3
4
5
6
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap

데이터 전송

웹소켓 통신은 프레임(frame)이라 불리는 데이터 조각을 사용해 이루어진다. 데이터 종류에 따라 다음과 같이 나뉜다.

  • 텍스트 프레임 (text frame) - 텍스트 데이터가 담긴 프레임
  • 이진 데이터 프레임 (binary data frame) - 이진 데이터가 담긴 프레임
  • 핑 또는 퐁 프레임 (ping/pong frame) - 커넥션이 유지되고 있는지 확인할 때 사용하는 프레임으로 서버나 브라우저에서 자동 생성해서 보내는 프레임
  • 이 외에도 커넥션 종료 프레임(connection close frame) 등 다양한 프레임이 있음.

브라우저에서 개발자는 텍스트나 이진 프레임만 다루게 된다. 이유는 socket.send(body) 에서 body 에 올수 있는 데이터가 string 과 blog, arraybuffer 같은 형태이기 때문이다.

받을 때는 텍스트 일경우 문자열로 들어온다. 이진데이터일 경우에는 선택할 수 있다.
이진 데이터를 받을 때는 socket.binaryType 프로퍼티를 사용하여 Blob 이나 ArrayBuffer 포맷 중 하나를 고룰 수 있다.

1
2
3
4
socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
// event.data는 (텍스트인 경우) 문자열이거나 (이진 데이터인 경우) arraybuffer 입니다.
};

Rate limiting (속도 제한)

느린 데이터 환경에서 데이터를 전송하고 있다고 생각해 보자. 사용자는 계속해서 send 하겠지만 데이터는 버퍼링 되어 메모리에 저장되고 충분히 전달하기에 빠른 환경에 있을때 보내지게 될 것이다.

socket.bufferedAmount 프로퍼티는 보내고 있는 시점에서 남아있는 바이트나 버퍼를 저장하고 있다. 이 정보를 활용해서 버퍼에 데이터가 없을때 send 하는 로직을 구성할 수 있다.

1
2
3
4
5
6
7
// every 100ms examine the socket and send more data
// only if all the existing data was sent out
setInterval(() => {
if (socket.bufferedAmount == 0) {
socket.send(moreData());
}
}, 100);

Connection close

일반적으로 연결을 종료하고 싶을때 숫자 코드와 이유가 포함된 연결 종료 프레임을 보낸다.

1
socket.close([code], [reason]);
  • code : 정해진 숫자 코드 (optional)
  • reason : 이유를 설하는 문자열 (optional)

예시 :

1
2
3
4
5
6
7
8
9
// closing party:
socket.close(1000, "Work complete");

// the other party
socket.onclose = (event) => {
// event.code === 1000
// event.reason === "Work complete"
// event.wasClean === true (clean close)
};

많이 사용하는 코드는 다음과 같다.

  • 1000 : default, normal closure
  • 1006 : 브라우저 구현에 의해 연결이 비정상적으로 (로컬로) 닫혔음을 의미하는 특수 코드

다른 코드는 다음과 같다.

  • 1001 : 서버가 꺼지거나 브라우저에서 페이지를 떠났다.
  • 1009 : 메세지가 처리하기에 너무 크다.
  • 1011 : 서버에 알수 없는 에러

WebSocket Code 는 HTTP 코드와 비슷한듯 다르다. 특별히 1000 미만의 코드는 미리 예약되어 있으며 설정하려고 하면 에러를 발생시킨다.

1
2
3
4
5
6
// in case connection is broken
socket.onclose = (event) => {
// event.code === 1006
// event.reason === ""
// event.wasClean === false (no closing frame)
};

Connection state

socket.readyState 프로퍼티를 사용하여서 연결 상태를 얻을 수 있다.

  • 0 : “CONNECTiNG” : 연결 중 아직 연결된 것은 아님
  • 1 : “OPEN”: communicating
  • 2 : “CLOSING” : the connection is closing
  • 3 : “CLOSED” : the connection is closed.

Chat example

웹소켓을 사용하는 체팅 예제를 살펴보자. 클라이언트에 집중해서 볼 것이지만, 서버도 간단하게 구현 가능하다.

메세지를 담을 form 이 필요하고, 메세지를 표시할 div 가 필요하다.

1
2
3
4
5
6
7
8
<!-- message form -->
<form name="publish">
<input type="text" name="message" />
<input type="submit" value="Send" />
</form>

<!-- div with messages -->
<div id="messages"></div>

자바스크립트를 통해서 해야 할것 :

  1. 연결
  2. socket.send(message)
  3. 수신 메세지 div에 표시

구현 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws")
// send message from the form
document.forms.publish.onsubmit = function() => {
let outgoingMessage = this.message.value;
socket.send(outgoingMessage);
return false;
}

// message received - show the message in div
socket.onmessage = function(event) => {
let message = event.data;
let messageElem = document.createElement('div');
messageElem.textContent = message;
docuemnt.getElementById('messages').prepend(messageElem);
}