0502 - 0508

0502

국제화, 내용 협상, 웹 호스팅, 부하 균형

국제화

HTTP에서 엔터티 본문이란 그저 비트들로 가득 찬 상자에 불과하다.
국제 콘텐츠를 지원하기 위해, 서버는 클라이언트에게 각 문서의 문자와 언어를 알려줘서, 사용자에게 콘텐츠를 제공해줄 수 있도록 할 필요가 있다.

서버는 클라이언트에게 문서의 문자와 언어를 Content-Language 헤더를 통해 알려준다.

문자 집합과 HTTP

  • charset 태그는 비트들을 글자들로 변환하거나 혹은 그 반대의 일을 해주는 알고리즘을 명명한다.
  • 문서를 이루는 비트들은, 특정 코딩된 문자집합의 특정 문자로 식별될 수 있는 문자 코드로 변환된다. 문자 코드는 코딩된 문자집합의 특정 요소를 선택하기 위해 사용된다.
  • 잘못된 charset 매개변수를 사용하면, 클라이언트는 이상한 깨진 글자를 보여주게 된다.
  • 문자 집합이 명시적으로 나열되지 않았다면, 수신자는 문서의 콘텐츠로부터 문서집합을 추측하려 시도한다. HTML에서는 메타 태그가 이에 해당한다.
  • 클라이언트는 서버에게 그들이 어떤 문자 체계를 지원하는지 Accept-Charset 요청 헤더를 통해 알려준다.

내용 협상과 트랜스코딩

클라이언트 주도 협상

클라이언트가 요청을 보내면 서버는 클라이언트에게 선택지를 보내주고, 클라이언트가 선택한다.
서버 입장에서는 가장 구현하기 쉽지만, 대기시간이 증가한다는 단점이 존재한다.

서버 주도 협상

서버가 클라이언트 요청 Accept 헤더를 검증해서 어떤 버전을 제공할지 결정한다.
HTTP 프로토콜은 클라이언트가 각 선호의 카테고리마다 여러 선택 가능한 항목을 선호도와 함께 나열할 수 있도록 품질값을 정의하였다.
품질값(q)은 0.0부터 1.0까지 가질 수 있다. 0.0이 가장 낮은 선호도, 1.0이 가장 높은 선호도를 의미한다.

투명 협상

클라이언트 입장에서 협상하는 중개자 프락시를 둠으로써 클라이언트와의 메시지 교환을 최소화하는 동시에 서버 주도 협상으로 인한 부하를 서버에서 제고한다.

트랜스 코딩

서버가 클라이언트의 요구에 맞는 문서를 아예 갖고 있지 않은 경우 서버는 기존의 문서를 클라이언트가 사용할 수 있는 무언가로 변환할 수 있는데, 이 옵션을 트랜스 코딩이라고 한다.

  • 포맷 변환 - 데이터를 클라이언트가 볼 수 있도록 한 포맷에서 다른 포맷으로 변환한다.
  • 정보 합성 - 문서에서 정보의 요점을 추출한다.
  • 콘텐츠 주입 - 오히려 양을 늘리는 또 다른 종류의 변환인 내용 주입 트랜스코딩이다.

가상 호스팅

많은 웹 호스팅 업자는 컴퓨터 한 대를 여러 고객이 공유하게 해서 저렴한 웹 호스팅 서비스를 제공한다. 이를 공유 호스팅 혹은 가상 호스팅이라 부른다.
각 웹 사이트는 다른 서버에서 호스팅하는 것처럼 보이겠지만, 물리적으로 같은 서버에서 호스팅되는 것이다.
호스팅 업자는 복제 서버 더미(서버 팜)를 만들고 서버 팜에 부하를 분산할 수 있다. 팜에 있는 각 서버는 다른 서버를 복제한 것이며, 수많은 가상 웹 사이트를 호스팅하고 있기 때문에 관리자는 훨씬 편해진다.

가상호스팅 동작하게 하기

  • URL 경로를 통한 가상 호스팅 - 서버가 어떤 사이트를 요청하는 것인지 알 수 있게 URL에 특별한 경로 컴포넌트를 추가한다. (좋지 않은 방법)
  • 포트번호를 통한 가상 호스팅 - 각 사이트에 다른 포트번호를 할당하여, 분리된 웹 서버의 인스턴스가 요청을 처리한다. (좋지 않은 방법)
  • IP 주소를 통한 가상 호스팅 - 각 가상 사이트에 별도의 IP 주소를 할당하고 모든 IP 주소를 장비 하나에 연결한다. (문제가 있지만 위의 두 방법보다는 좋은 방법)
  • HOST 헤더를 통한 가상 호스팅 - 웹 서버는 Host 헤더로 가상 사이트를 식별할 수 있다. (가장 좋은 방법)

리다이렉션과 부하 균형

HTTP 애플리케이션은 세 가지를 원한다.

  • 신뢰할 수 있는 HTTP 트랜잭션의 수행
  • 지연 최소화
  • 네트워크 대역폭 절약

이러한 이유 때문에 웹 콘텐츠는 여러 장소에 배포된다. 이렇게 하면 한 곳에 실패한 경우 다른 곳을 이용할 수 있으므로 신뢰성이 개선된다.
또한 클라이언트가 보다 가까운 리소스에 접근할 수 있게 되어 응답시간도 줄여준다.
그리고 목적지 서버가 분산되므로 네트워크 혼잡도 줄어든다.

-> 리다이렉션이란 최적의 분산된 콘텐츠를 찾는 것을 도와주는 기법을 의미한다.

대부분의 리다이렉션 장치들은 몇 가지 방식의 부하 균형을 포함한다. 즉, 들어오는 메시지의 부하를 서버들의 집합에게 분산할 수 있다.

HTTP 리다이렉션

몇몇 웹 사이트는 HTTP 리다이렉트 메시지를 이용해 부하를 분산한다.
요청을 처리하는 서버(리다이렉팅 서버)는 가용한 것들 중 부하가 가장 적은 콘텐츠 서버를 찾아서 브라우저의 요청을 그 서버로 리다이렉트 한다.

장점
리다이렉트를 하는 서버가 클라이언트의 아이피 주소를 알기 때문에 좀 더 정보에 근거해 선택할 수 있다.

단점

  • 어떤 서버로 리다이렉트할 지 결정하려면 원 서버는 상당히 많은 처리를 해야 한다.
  • 페이지에 접근할 때마다 두 번의 왕복이 필요하기 때문에, 대기 시간이 길어진다.
  • 리다이렉트 서버가 고장나면, 사이트도 고장난다.

DNS 리다이렉션

DNS는 하나의 도메인에 여러 아이피 주소가 결부되는 것을 허용하며, DNS 분석자는 여러 아이피 주소를 반환하도록 설정되거나 프로그래밍 될 수 있다.

분석자가 어떤 아이피 주소를 반환할 것인가를 결정하는 방법은 단순한 것부터 복잡한 것까지 다양하다. 가장 쉬운 DNS 결정 알고리즘은 단순한 라운드 로빈이다.

라운드 로빈
서버에 대한 클라이언트의 상대적인 위치나 서버의 현재 스트레스를 고려하지 않는다.
부하 균형을 위해, DNS 서버는 룩업이 끝났을 때마다 주소를 순환시킨다.
쉽게 말해서, 부하를 순환시켜 돌아가면서 서버를 배정하는 것이다.
-> 완벽하지는 않은 방법

몇몇 향상된 DNS 서버는 주소의 순서를 결정하기 위해 다른 기법을 사용한다.

부하 균형 알고리즘
웹 서버의 로드를 추적하고 가장 로드가 적은 웹 서버를 목록의 가장 위에 놓는다.

근접 라우팅 알고리즘
근처의 웹 서버로 보내는 시도를 한다.

결함 마스킹 알고리즘
DNS 서버는 네트워크의 상태를 모니터링하여 장애를 피해서 라우팅한다.




0503 TIL

리덕스

리덕스를 사용하면 컴포넌트의 상태 업데이트 관련 로직을 다른 파일로 분리시켜서 더욱 효율적으로 관리할 수 있다.
단순히 전역 상태 관리만 한다면 Context API를 사용하는 것만으로도 충분할 수 있다.
하지만 리덕스를 사용하면 상태를 더욱 체계적으로 관리할 수 있기 때문에 프로젝트의 규모가 클 경우에는 리덕스를 사용하는 것이 좋다. 코드의 유지 보수성도 높여 주고 작업 효율도 극대화해 주기 때문이다.
추가로 아주 편리한 개발자 도구를 지원하며, 미들웨어라는 기능을 제공하여 비동기 작업을 훨씬 효율적으로 관리할 수 있게 해 준다.

상태 관리의 필요성

애플리케이션의 상태 관리가 복잡해진 만큼 버그를 최소화하고, 잘 만들어진 UX를 제공하는 데 있어 상태를 효과적으로 관리할 필요성이 생겼다.
여기서 상태란 서버 응답, 캐시 데이터, 로컬 상태(아직 서버에 저장되지 않은 데이터) 등을 의미한다.
뿐만 아니라 활성화 된 라우트, 선택된 탭 핸들, 로딩 표시 여부, 페이지네이션 컨트롤 등 다양한 UI View 상태도 해당된다.

리덕스는 다음과 같은 상황에 사용하기 적절하다.

  • 지속적으로 업데이트 되는 상당한 양의 상태가 있다.
  • 상태 업데이트를 위한 단 1개의 스토어가 필요하다.
  • 최상위 루트 컴포넌트가 모든 상태를 관리하는 것은 적절하지 않다.

개념

액션

상태 변경을 설명하는 정보. 상태에 어떠한 변화가 필요하면 액션이 발생한다. 이는 하나의 객체로 표현된다.

{
  type: 'ADD_TODO',
  data: {
    id: 1,
    text: '리덕스 배우기',
  }
}
  • 액션 객체는 type 필드를 반드시 가지고 있어야 한다. 이 값을 액션의 이름이라고 생각하면 된다.
  • 그 외의 값들은 나중에 상태 업데이트를 할 때 참고해야 할 값이며, 작성자 마음대로 넣을 수 있다.
  • 액션 타입을 상수로 관리하는 것이 유지 관리하기 좋다.
  • 액션 타입은 대문자로 정의하고, 문자열 내용은 ‘모듈 이름/액션 이름’과 같은 형태로 작성한다. 문자열 안에 모듈 이름을 넣음으로써, 나중에 프로젝트가 커졌을 때 액션의 이름이 충돌되지 않게 해 준다.

액션 생성 함수

액션 생성 함수는 액션 객체를 만들어 주는 함수이다.

const addTodo = (data) => ({
  type: "ADD_TODO",
  data,
});
  • 어떤 변화를 일으켜야 할 때마다 액션 객체를 만들어야 하는데 매번 액션 객체를 직접 작성하기 번거로울 수 있고, 만드는 과정에서 실수로 정보를 놓칠 수도 있다. 이런 일을 방지하기 위해 이를 함수로 만들어서 관리한다.
  • export를 넣어줘야 다른 파일에서 불러와 사용할 수 있다.

리듀서

리듀서는 변화를 일으키는 함수이다. 액션을 만들어서 발생시키면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아 온다.
그리고 두 값을 참고하여 새로운 상태를 만들어서 반환해 준다.

const initialState = {
  counter: 1,
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case "INCREMENT":
      return {
        counter: state.counter + 1,
      };
    default:
      return state;
  }
}
  • 리듀서에서는 상태의 불변성을 유지하면서 데이터에 변화를 일으켜 주어야 한다.
  • 객체의 구조가 복잡해지면 불변성을 관리하며 업데이트하는 것이 굉장히 번거로울 수 있고 코드의 가독성도 나빠지기 때문에 리덕스의 상태는 최대한 깊지 않은 구조로 진행하는 것이 좋다.

스토어

한 개의 프로젝트는 단 하나의 스토어만 가질 수 있다.
스토어 안에는 현재 애플리케이션 상태와 리듀서가 들어가 있으며, 그 외에도 몇 가지 중요한 내장 함수를 지닌다.

디스패치

디스패치는 스토어의 내장 함수 중 하나이다.
디스패치는 액션 객체를 파라미터로 넣어서 호출하고, 이 함수가 호출되면 스토어는 리듀서 함수를 실행시켜서 새로운 상태를 만든다.

구독

구독도 스토어의 내장 함수 중 하나이다.
subscribe 함수 안에 리스너 함수를 파라미터로 넣어서 호출해 주면, 이 리스너 함수가 액션이 디스패치되어 상태가 업데이트될 때마다 호출된다.

const listener = () => {
  console.log("상태가 업데이트됨");
};

const unsubscribe = store.subscribe(listener);

unsubscribe(); // 추후 구독을 비활성화할 때 함수를 호출

리덕스의 세 가지 규칙

단일 스토어 (동기화 필요 x, 디버깅 용이)

하나의 애플리케이션 안에는 하나의 스토어가 들어 있다.
여러 개의 스토어를 사용하는 것이 완전히 불가능하지는 않다. 특정 업데이트가 너무 빈번하게 일어나거나 애플리케이션의 특정 부분을 완전히 분리시킬 때 여러 개의 스토어를 만들 수도 있지만, 상태 관리가 복잡해질 수 있으므로 권장하지 않는다.

읽기 전용 상태 (예측 가능)

리덕스 상태는 읽기 전용이다. 상태를 업데이트 할 때 기존의 객체는 건드리지 않고 새로운 객체를 생성해 주어야 한다.
리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 비교 검사를 해서 좋은 성능을 유지하기 위한 것이다.

리듀서는 순수한 함수

변화를 일으키는 리듀서 함수는 순수한 함수여야 한다. 순수 함수는 다음 조건을 만족한다.

  • 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다.
  • 파라미터 외의 값에는 의존하면 안 된다.
  • 이전 상태는 절대로 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어서 반환한다.
  • 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환해야 한다.

리덕스 디렉터리 구조

일반적인 구조

actions, constants, reducers라는 세 개의 디렉터리를 만들고 그 안에 기능별로 파일을 하나씩 만드는 방식이다.
코드를 종류에 따라 다른 파일에 작성하여 정리할 수있어서 편리하지만, 새로운 액션을 만들 때마다 세 종류의 파일을 모두 수정해야 하기 때문에 불편하기도 하다.

Ducks 패턴

modules 폴더에 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식

리듀서를 여러 개로 만들었을 때, 스토어를 만들 때는 리듀서를 하나만 사용해야 한다. 그렇기 때문에 기존에 만들었던 리듀서를 하나로 합쳐 주어야 하는데, 이 작업은 리덕스에서 제공하는 combineReducers라는 유틸 함수를 사용하면 쉽게 처리할 수 있다.

const rootReducer = combineReducers({
  counter,
  todos,
});

export default rootReducer;

리액트 애플리케이션에 리덕스 적용

import { createStore } from "redux";
import rootReducer from "./modules";
import { Provider } from "react-redux";

const store = createStore(
  rootReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
  1. index.js에서 스토어를 생성하여 첫 번째 인자로 리듀서를 전달한다.
  2. 두 번째 인자는 리덕스 tools를 사용하기 위해 전달하는 것이다.
  3. 리액트 컴포넌트에서 스토어를 사용할 수 있도록 App 컴포넌트를 Provider 컴포넌트로 감싸 준다. store를 props로 전달해 주어야 한다.

스토어를 store/configureStore.js로 따로 빼서 구성에만 집중할 수도 있다.




0504

HTTP 완벽가이드 스터디 회고

이펙티브 타입스크립트 스터디에 이어서 두 번째 온라인 스터디가 막을 내렸다.
책의 내용이 오래되어서 공감이 어려운 부분도 많았지만, 그래도 HTTP와 관련된 많은 지식을 얻을 수 있었다고 생각한다.
이번 스터디를 통해 얻은 것은 다음과 같다.

  • HTTP에 대한 깊은 이해
  • 네트워크 & HTTP와 관련된 기반 지식

마지막 장으로 갈수록 내가 지금 당장 알아야하는 개념인가? 하는 의문이 들어서 집중도가 떨어지기도 했지만, 스터디를 함께 하는 팀원들과 회의를 통해 학습 분량을 조절함으로서 위기를 극복할 수 있었다.

어려운 점도 많았지만 얻은 것이 더 많은 스터디였다. 다만 일회독을 하며 내용을 이해한 것이고 온전히 내 것으로 만들기 위해서는 추가적인 복습이 필요할 것 같다.



팀원들과의 회고

회고 1 회고 2 회고 3 회고 4




0505

고양이 사진첩 만들기

이전부터 바닐라 JS로 컴포넌트 방식 개발하는 데에 관심이 있었는데, 이번 기회에 날 잡고 도전 및 정리해보고자 한다.

선언적 프로그래밍과 컴포넌트 추상화

DOM을 접근하는 부분을 최소화하고, 명령형 프로그래밍 방식보다는 선언적 프로그래밍 방식으로 접근하는 것을 의미한다.
Node 데이터를 화면에 그리는 것을 기준으로 하면, 명령형 프로그래밍 방식의 경우 다음과 같다.

function render(nodes) {
  const $container = document.querySelector('.container');
  nodes.forEach(node => {
    ...
  })
}

위의 코드의 경우 크게 문제가 되는 것은 아니지만, DOM에 접근하고 업데이트하는 시점에 대한 명확한 기준점이 없기 때문에, 코드가 거대해지고 UI의 업데이트가 많아질 경우 어느 지점에서 어느 시점에 DOM을 업데이트 했느냐를 추적하기가 점점 힘들어진다.

function Nodes({ $app, initialState }) {
  this.state = initialState;

  this.$target = document.createElement("ul");
  $app.appendChild(this.$target);

  this.state = (nextState) => {
    this.state = nextState;

    this.render();
  };

  this.render = () => {
    this.$target.innerHTML = this.state.nodes.map(
      (node) => `<li>${node.name}</li>`
    );
  };

  this.render();
}
  • constructor - new 키워드를 통해 해당 컴포넌트가 생성되는 시점에 실행된다. 해당 컴포넌트가 표현될 element를 생성하고, 파라미터로 받은 $app에 렌더링하도록 한다.
  • render - 해당 컴포넌트의 state를 기준으로 자신의 element에 렌더링한다. 자신의 상태를 기준으로 렌더링해야 하기 때문에, 별도의 파라미터를 받지 않아야 한다.
  • setState - 해당 컴포넌트의 state를 갱신한다. render 함수가 별도의 파라미터 없이 자신의 상태를 기준으로 렌더링하도록 작성이 되어있기 때문에, state를 변경하고 다시 render 함수를 부르도록 함으로써 업데이트된 상태를 화면에 반영할 수 있게 된다.

이런 식으로 작성하면, 실제 DOM을 직접 제어하는 부분을 컴포넌트가 인스턴스화 되는 시점, 그리고 render 함수가 다시 호출되는 시점으로 제한할 수 있다.

컴포넌트간의 의존도 줄이기

위의 코드는 단순히 상태를 기준으로 렌더링만 하는 코드이고, 실제 UI 인터렉션에 따라 state를 변경해야하는 경우 a 컴포넌트에서 일어나는 인터렉션에 의해 b 컴포넌트에 영향을 줄 수 있다.

이때, a 코드 내에서 b 컴포넌트를 직접 다루거나 업데이트 하도록 코드를 작성하게 되면 a 컴포넌트를 독립적으로 사용할 수 없게 된다. b에 의존성이 생기기 때문이다. 즉, b가 필요없이 a만 필요한 화면에서는 쓸 수가 없게 된다.

이런 경우 일반적으로 두 컴포넌트를 조율하는 더 상위의 컴포넌트를 만들고, 콜백 함수를 통해 느슨하게 결합한다. a를 클릭했을 때, b의 변화를 줘야 한다면 a의 파라미터로 onClick 이벤트 핸들러를 받고, render 함수를 수정한다.

function Nodes({ $app, initialState, onClick }) {
  this.state = initialState;
  this.onClick = onClick;

  this.$target = document.createElement("div");
  $app.appendChild(this.$target);

  this.state = (nextState) => {
    this.state = nextState;
    this.render();
  };

  this.render = () => {
    if (this.state.nodes) {
      const nodesTemplate = this.state.nodes
        .map((node) => {
          const iconPath =
            node.type === "FILE"
              ? "./assets/file.png"
              : "./assets/directory.png";

          return `
                    <div class="Node" data-node-id="${node.id}">
                        <img src=${iconPath} />
                        <div>${node.name}</div>
                    </div>
                `;
        })
        .join("");

      this.$target.innerHTML = !this.state.isRoot
        ? `<div class="Node"><img src="/assets/prev.png"></div>${nodesTemplate}`
        : nodesTemplate;
    }

    this.$target.querySelectorAll(".Node").forEach(($node) => {
      $node.addEventListener("click", (e) => {
        const { nodeId } = e.target.dataset;
        const selectedNode = this.state.nodes.find(
          (node) => node.id === nodeId
        );

        if (selectedNode) {
          this.onClick(selectedNode);
        }
      });
    });
  };
  this.render();
}
  • render 함수에서 렌더링된 이후 클릭 가능한 모든 요소에 click 이벤트를 건다.
function App($app) {
  this.state = {
    isRoot: false,
    nodes: [],
    depth: [],
  };

  const breadcrumb = new Breadcrumb({
    $app,
    initialState: this.state.depth,
  });
  const nodes = new Nodes({
    $app,
    initialState: {
      isRoot: this.state.isRoot,
      nodes: this.state.nodes,
    },
    onClick: (node) => {
      if (node.type === "DIRECTORY") {
        // DIRECTORY인 경우 처리
      } else if (node.type === "FILE") {
        // FILE인 경우 처리
      }
    },
  });
}
  • Nodes와 Breadcrumb를 조율하기 위한 App 컴포넌트이다.
  • onClick에는 함수를 파라미터로 던지고, Nodes 내에서 클릭 발생 시 이 함수를 호출하게 한다. 그렇게 하면 Nodes 내에선 클릭 후 어떤 로직이 일어날지 알아야 할 필요가 없다.
  • 이렇게 하면 App이 두 컴포넌트를 조율하는 형태가 되며, 두 컴포넌트는 독립적으로 동작하고 또 다른 곳에 쉽게 재활용 할 수 있는 구조가 된다.

태그:

카테고리:

업데이트: