0418 - 0424

0418

보안 HTTP

HTTP를 안전하게 만들기

대량 구매, 은행 업무, 혹은 보안 자료 접근과 같은 중요한 트랜잭션을 위해서는, HTTP와 디지털 암호화 기술을 결합해야 한다.

다음을 제공해 줄 수 있는 HTTP 보안 기술이 필요하다.

  • 서버 인증 - 클라이언트는 자신이 위조된 서버가 아닌 진짜와 이야기하고 있음을 알 수 있어야 한다.
  • 클라이언트 인증 - 서버는 자신이 가짜가 아닌 진짜 사용자와 이야기하고 있음을 알 수 있어야 한다.
  • 무결성 - 클라이언트와 서버는 그들의 데이터가 위조되는 것으로부터 안전해야 한다.
  • 암호화 - 클라이언트와 서버는 도청에 대한 걱정 없이 서로 대화할 수 있어야 한다.
  • 효율 - 저렴한 클라이언트나 서버도 이용할 수 있도록 알고리즘은 충분히 빨라야 한다.
  • 편재성 - 프로토콜은 거의 모든 클라이언트와 서버에서 지원되어야 한다.
  • 관리상 확장성 - 누구든 어디서든 즉각적인 보안 통신을 할 수 있어야 한다.
  • 적응성 - 현재 알려진 최선의 보안 방법을 지원해야 한다.
  • 사회적 생존성 - 사회의 문화적, 정치적 요구를 만족시켜야 한다.

HTTPS

  • HTTPS는 HTTP를 안전하게 만드는 방식 중에서 가장 인기있는 것이다.
  • HTTP를 사용할 때, 모든 HTTP 요청과 응답 데이터는 네트워크로 보내지기 전에 암호화된다.
  • HTTPS는 HTTP의 하부에 전송 레벨 암호 보안 계층을 제공함으로써 동작하는데, 이 보안 계층은 안전 소켓 계층(SSL - secure sockets layer), 혹은 그를 계승한 전송 계층 보안(TLS)을 이용하여 구현된다.
  • 어려운 인코딩 및 디코딩 작업은 대부분 SSL 라이브러리 안에서 일어나기 때문에, 보안 HTTP를 사용하기 위해 웹 클라이언트와 서버가 프로토콜을 처리하는 로직을 크게 변경할 필요는 없다.

디지털 암호학

용어

  • 암호 - 텍스트를 아무나 읽지 못하도록 인코딩하는 알고리즘
  • 키 - 암호의 동작을 변경하는 숫자로 된 매개변수
  • 대칭키 암호 체계 - 인코딩과 디코딩에 같은 키를 사용하는 알고리즘
  • 비대칭키 암호 체계 - 인코딩과 디코딩에 다른 키를 사용하는 알고리즘
  • 공개키 암호 체계 - 비밀 메시지를 전달하는 수백만 대의 컴퓨터를 쉽게 만들 수 있는 시스템
  • 디지털 서명 - 메시지가 위조 혹은 변조되지 않았음을 입증하는 체크섬
  • 디지털 인증서 - 신뢰할 만한 조직에 의해 서명되고 검증된 신원 확인 정보

암호

암호란 메시지를 인코딩하는 어떤 특정한 방법과 나중에 그 비밀 메시지를 디코딩하는 방법이다.
인코딩되기 전의 원본 메시지는 흔히 텍스트 혹은 평문이라고 불린다.
암호가 적용되어 코딩된 메시지는 보통 암호문이라고 불린다.

키가 있는 암호

코드 알고리즘과 기계가 적의 손에 들어갈 수 있기 때문에, 대부분의 기계들에는 암호의 동작방식을 변경할 수 있는 큰 숫자로 된 다른 값을 설정할 수 있는 다이얼이 달려있다.
누군가 기계를 훔치더라도, 올바른 다이얼 설정(키 값)이 없이는 디코더가 동작하지 않을 것이다.
이러한 암호 매개변수를 라고 부른다.

디지털 암호

디지털 계산의 도래로, 두 가지 주요한 발전이 있었다.

  • 속도 및 기능에 대한 기계 장치의 한계에서 벗어남으로써, 복잡한 인코딩과 디코딩 알고리즘이 가능해졌다.
  • 매우 큰 키를 지원하는 것이 가능해져서, 단일 암호 알고리즘으로 키의 값마다 다른 수조 개의 가상 알고리즘을 만들어낼 수 있게 되었다.

대칭키 암호법

많은 디지털 암호 알고리즘은 대칭키 암호라 불리는데, 인코딩을 할 때 사용하는 키가 디코딩을 할 때와 같기 때문이다.
대칭키 암호에서, 발송자와 수신자 모두 통신을 위해 비밀 키를 똑같이 공유할 필요가 있다.

ex) DES, RC2, RC4

키 길이와 열거 공격

비밀 키는 누설되면 안 된다. 대부분의 경우, 인코딩 및 디코딩 알고리즘은 공개적으로 알려져 있으므로, 키만이 유일한 비밀이다.
좋은 암호 알고리즘은 공격자가 코드를 크래킹하려면 모든 가능한 키 값을 시도해보는 것 외에 다른 방법이 없게 만든다.
무차별로 모든 키 값을 대입해보는 공격을 열거 공격이라고 한다.
만약 가능한 키 값이 몇 가지 밖에 없다면 악당은 무차별 대입으로 모든 값을 시도하고 결국 암호를 깨게 될 것이다.

가능한 키 값의 개수는 키가 몇 비트이며 얼마나 많은 키가 유효한지에 달려있다.
대칭키 암호에서는, 보통 모든 키 값이 유효하다.

대칭키 암호의 단점 중 하나는 발송자와 수신자가 서로 대화하려면 둘 다 공유키를 가져야 한다는 것이다.

공개키 암호법

한 쌍의 호스트가 하나의 인코딩/디코딩 키를 사용하는 대신, 공개키 암호 방식은 두 개의 비대칭 키를 사용한다.
인코딩 키는 모두를 위해 공개되어 있다. 하지만 호스트만이 개인 디코딩 키를 알고 있다.
이는 노드가 서버로 안전하게 메시지를 발송하는 것을 더 쉽게 해주는데, 왜냐하면 서버의 공개 키만 있으면 되기 때문이다.
공개키 암호화 기술은 보안 프로토콜을 전 세계의 모든 컴퓨터 사용자에게 적용하는 것을 가능하게 했다.

RSA

공개키 비대칭 암호의 과제는 해커가 공개키, 가로챈 암호문의 일부, 메시지와 그것을 암호화한 암호문을 알고 있다 해도 비밀인 개인 키를 계산할 수 없다는 것을 확신시켜 주는 것이다.

이를 만족하는 공개키 암호 체계 중 유명한 하나는 RSA 알고리즘이다.

혼성 암호 체계와 세션 키

공개키 암호 방식의 알고리즘은 계산이 느린 경향이 있다.
실제로는 대칭과 비대칭 방식을 석은 것이 쓰인다.
노드들 사이의 안전한 의사소통 채널을 수립할 때는 공개 키 암호를 사용하고, 나머지 데이터를 암호화할 때는 빠른 대칭 키를 사용하는 방식이 흔히 쓰인다.

디지털 서명

암호 체계는 메시지를 암호화하고 해독하는 것뿐 아니라, 누가 메시지를 썼는지 알려주고 그 메시지가 위조되지 않았음을 증명하기 위해 메시지에 서명을 하는 데에 이용될 수 있다.

서명은 암호 체크섬이다

디지털 서명은 메시지에 붙어있는 특별한 암호 체크섬이다.

  • 서명은 메시지를 작성한 저자가 누군지 알려준다. 저자는 개인 키를 갖고 있기 때문에, 오직 저자만이 이 체크섬을 계산할 수 있다. 체크섬은 저자의 개인 서명처럼 동작한다.
  • 서명은 메시지 위조를 방지한다. 만약 악의적인 공격자가 송신 중인 메시지를 수정했다면, 체크섬은 더 이상 그 메시지와 맞지 않게 될 것이다. 체크섬은 저자의 비밀 개인 키에 관련되어 있기 때문에, 날조해낼 수 없을 것이다.

디지털 서명은 보통 비대칭 공개키에 의해 생성된다.
예를 들어 A가 정제된 메시지에 개인 키를 매개변수로 하는 서명 함수를 적용해서 메시지의 끝에 덧붙이고 전송하면, B는 서명을 검사할 수 있다.
노드 B는 개인 키로 알아보기 어렵게 변형된 서명에 공개키를 이용한 역함수를 적용하여 일치하지 않는다면, 위조되었거나 노드 A가 메시지를 쓴 것이 아니라고 판단한다.

디지털 인증서

디지털 인증서는 신뢰할 수 있는 기관으로부터 보증 받은 사용자나 회사에 대한 정보를 담고 있다.
인증서의 내부에는 인증 기관에 의해 디지털 서명된 정보의 집합이 담겨있다.

  • 대상의 이름
  • 유효 기간
  • 인증서 발급자
  • 인증서 발급자의 디지털 서명

디지털 인증서는 대상과 사용된 서명 알고리즘에 대한 서술적인 정보뿐 아니라 보통 대상의 공개키도 담고 있다.
누구나 디지털 인증서를 만들 수 있지만, 그 모두가 인증서의 정보를 보증하고 인증서를 개인 키로 서명할 수 있는 널리 인정받는 서명 권한을 얻을 수 있는 것은 아니다.

서버 인증을 위해 인증서 사용하기

사용자가 HTTPS를 통한 안전한 웹 트랜잭션을 시작할 때, 최신 브라우저는 자동으로 접속한 서버에서 디지털 인증서를 가져온다. 만약 서버가 인증서를 갖고 있지 않다면, 보안 커넥션은 실패한다.

브라우저가 인증서를 받으면, 서명 기관을 검사한다. 만약 그 기관이 공공이 신뢰할만한 서명 기관이라면 브라우저는 그것의 공개키를 이미 알고 있을 것이며, 그 서명을 검증할 수 있다.

HTTPS의 세부사항

HTTPS는 HTTP 프로토콜에 대칭, 비대칭 인증서 기반 암호 기법의 강력한 집합을 결합한 것이다.
HTTPS는 웹 기반 전자상거래의 고속 성장과 광역 보완 관리에 있어 대단히 중요하다.

HTTPS 개요

HTTPS는 그냥 보안 전송 계층을 통해 전송되는 HTTP이다.
암호화되지 않은 HTTP 메시지를 TCP를 통해 전 세계의 인터넷 곳곳으로 보내는 대신에, HTTP 메시지를 TCP로 보내기 전에 먼저 그것들을 암호화하는 보안 계층을 보낸다.
오늘날 HTTPS의 보안 계층은, SSL로 구현되었다.

HTTPS 스킴

보안 HTTP는 선택적이기에 웹 서버로의 요청을 만들 때, 웹 서버에게 HTTP의 보안 프로토콜 버전을 수행한다고 말해줄 방법이 필요하다.
이것은 URL의 스킴 https를 통해 이루어진다.

보안 전송 셋업

HTTPS에서, 클라이언트는 먼저 웹 서버의 443 포트로 연결한다.
일단 연결이 되고 나면, 클라이언트와 서버는 암호법 매개변수와 교환 키를 협상하면서 SSL 계층을 초기화한다.
핸드셰이크가 완료되면 SSL 초기화는 완료되며, 클라이언트는 요청 메시지를 보안 계층에 보낼 수 있다.
이 메시지는 TCP로 보내지기 전에 암호화된다.

SSL 핸드셰이크

암호화한 HTTP 메시지를 보낼 수 있게 되기 전에, 클라이언트와 서버는 SSL 핸드셰이크를 할 필요가 있다.

  • 프로토콜 버전 번호 교환
  • 양쪽이 알고 있는 암호 선택
  • 양쪽의 신원을 인증
  • 채널을 암호화하기 위한 임시 세션 키 생성
  1. 클라이언트가 암호 후보들을 보내고 인증서를 요구한다.
  2. 서버는 선택된 암호와 인증서를 보낸다.
  3. 클라이언트가 비밀정보를 보낸다. 클라이언트와 서버는 키를 만든다.
  4. 클라이언트와 서버는 서로에게 암호화를 시작한다고 말해준다.

서버 인증서

SSL은 서버 인증서를 클라이언트로 나르고 다시 클라이언트 인증서를 서버로 날라주는 상호 인증을 지원한다.
그러나 클라이언트 인증서는 웹 브라우징에서는 흔히 쓰이지 않는다.

보안 HTTPS 트랜잭션은 항상 서버 인증서를 요구한다.
누군가가 웹 서버에 신용카드 정보를 보내는 것과 같은 보안 트랜잭션을 수행할 때, 대화 중인 조직이 생각한 조직이 맞는지 알고 싶을 것이다.
사용자는 모든 것이 믿을 만한 것인지 확인하기 위해 인증서를 검정할 수 있다.

사이트 인증서 검사

SSL 자체는 사용자에게 웹 서버 인증서를 검증할 것을 요구하지 않지만, 최신 웹 브라우저들 대부분은 인증서와 함께 기본적인 검사를 하고 그 결과를 더 철저한 검사를 할 수 있는 방법과 함께 사용자에게 알려준다.

  1. 날짜 검사
  2. 서명자 신뢰도 검사
  3. 서명 검사
  4. 사이트 신원 검사

가상 호스팅과 인증서

가상 호스트(하나의 서버에 여러 호스트 명)로 운영되는 사이트의 보안 트래픽을 다루는 것은 까다로운 경우도 많다.
만약 사용자가 인증서의 이름과 정확히 맞지 않는 가상 호스트 명에 도착했다면 경고 상자가 나타날 것이다.

프락시를 통한 보안 트래픽 터널링

클라이언트가 서버로 보낼 데이터를 서버의 공개키로 암호화하기 시작했다면, 프락시는 더 이상 HTTP 헤더를 읽을 수 없다.
이를 해결하기 위한 기법 하나는 HTTPS SSL 터널링 프로토콜이다.
HTTPS 터널링 프로토콜을 사용해서, 클라이언트는 먼저 프락시에게 자신이 연결하고자 하는 안전한 호스트와 포트를 말해준다. 클라이언트는 프락시가 읽을 수 있도록 암호화가 시작되기 전의 평문으로 말해준다.

CONNECT라 불리는 새로운 확장 메서드를 이용해서 프락시에게 희망하는 호스트와 포트번호로 연결을 해달라고 말해주며, 그것이 완료되면 클라이언트와 서버 사이에 데이터가 직접적으로 오갈 수 있게 해주는 터널을 만든다.


reference
HTTP 완벽 가이드




0419

컴포넌트 성능 최적화

많은 데이터를 렌더링 할 경우 애플리케이션이 느려지는 것을 체감할 수 있을 정도로 지연이 발생할 수 있다.

크롬 개발자 도구를 통한 성능 모니터링

성능을 분석해야 할 때는 느려졌다는 느낌만으로 충분하지 않다.
정확히 몇 초가 걸리는지 확인해야 하는데, React DevTools를 사용하여 측정하면 된다.
Profiler 탭에서 녹화하고 동작을 수행하여 성능 분석 결과를 확인할 수 있다.
여기서 Render duration은 리렌더링에 소요된 시간을 의미한다.

리액트 컴포넌트가 느려지는 원인

컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다.

  1. 자신이 전달받은 props가 변경될 때
  2. 자신의 state가 바뀔 때
  3. 부모 컴포넌트가 리렌더링될 때
  4. forceUpdate 함수가 실행될 때

예를 들어, 부모 컴포넌트에서 가지고 있는 상태 배열에서 하나의 값이 변경되었을 때, 자식 컴포넌트 중 값이 변경된 컴포넌트만 다시 리렌더링되면 되는 경우에도 부모 컴포넌트가 리렌더링되었읜 모두 리렌더링된다. 컴포넌트의 개수가 많지 않다면 모든 컴포넌트를 리렌더링해도 느려지지 않는데, 2000개가 넘어가면 성능이 저하된다.
이럴 때는 컴포넌트 리렌더링 성능을 최적화해 주는 작업을 해 주어야 한다.

React.memo를 사용하여 컴포넌트 성능 최적화

컴포넌트의 리렌더링을 방지할 때는 shouldComponentUpdate라는 라이프사이클을 사용하면 된다.
그런데 함수 컴포넌트에서는 라이프사이클 메서드를 사용할 수 없다.
그 대신 React.memo라는 함수를 사용한다. 컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 함수 컴포넌트의 리렌더링 성능을 최적화해 줄 수 있다.

React.memo를 사용하는 것만으로 컴포넌트 최적화가 끝나지는 않는다.
useCallback으로 감싼 함수에서 상태에 의존하는 경우, 상태가 변경되면 함수도 새롭게 바뀌기 때문이다.
이렇게 함수가 계속 만들어지는 상황을 방지하는 방법은 두 가지이다.

  1. useState의 함수형 업데이트 기능 사용
  2. useReducer 사용

useState의 함수형 업데이트

set.. 함수를 사용할 때 새로운 상태를 파라미터로 넣어 주는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣는다. 이를 함수형 업데이트라고 부른다.

const [number, setNumber] = useState(0);
const onIncrease = useCallback(
  () => setNumber((prevNumber) => prevNumber + 1),
  []
);

위 코드처럼 새로운 상태를 파라미터로 넣어 주는 것이 아니라, 어떻게 업데이트할지 정의해 주는 업데이트 함수를 넣어 주면 useCallback을 사용할 때 두 번째 파라미터로 넣는 배열에 number를 넣지 않아도 된다.
그렇기에 상태가 변경되어도 함수가 새로 생성되지 않는다.

useReducer 사용

useState의 함수형 업데이트를 사용하는 대신, useReducer를 사용해도 함수가 계속 새로워지는 문제를 해결할 수 있다.
useReducer를 사용할 때는 원래 두 번째 파라미터에 초기 상태를 넣어 주어야 한다. 하지만 초기 상태로 함수를 전달할 경우 두 번째 인수에 undefined를 넣고, 세 번째 파라미터에 함수를 넣어 준다. 이렇게 하면 컴포넌트가 맨 처음 렌더링될 때만 함수가 호출된다.

리액트 불변성

리액트 컴포넌트에서 상태를 업데이트할 때 불변성을 지키는 것은 매우 중요하다.
기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 불변성을 지킨다고 한다.
불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다.
그렇다면 예를 들어 React.memo에서 서로 비교하여 최적화하는 것이 불가능하다.

만약 객체 안에 있는 객체 혹은 배열이라면 불변성을 지키면서 새 값을 할당하기가 쉽지 않다.
구조가 정말 복잡해진다면 이렇게 불변성을 유지하면서 업데이트하는 것도 까다로워지는데, 복잡한 상황일 경우 immer 라이브러리의 도움을 받아 편하게 작업할 수 있다.

리스트 컴포넌트 최적화하기

리스트 관련 컴포넌트를 작성할 때는 리스트 아이템과 리스트, 이 두 가지 컴포넌트를 최적화해주어야 한다.
그러나 내부 데이터가 100개를 넘지 않거나 업데이트가 자주 발생하지 않는다면, 이런 최적화 작업을 반드시 해 줄 필요는 없다.

react-virtualized를 사용한 렌더링 최적화

리스트의 데이터가 2500개 등록되어 있는데, 실제 화면에 나오는 항목은 아홉 개뿐인 경우, 2491개 컴포넌트는 스크롤하기 전에는 보이지 않음에도 불구하고 렌더링이 이루어진다. 이는 비효율적이다.

react-virtualized를 사용하면 리스트 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 할 수 있다.
만약 스크롤되면 해당 스크롤 위치에서 보여 주어야 할 컴포넌트를 자연스럽게 렌더링시킨다.
이 라이브러리를 사용하면 낭비되는 자원을 아주 쉽게 아낄 수 있다.

import { memo, useCallback } from "react";
import { List } from "react-virtualized";
import TodoListItem from "./TodoListItem";
import "./TodoList.scss";

const TodoList = ({ todos, onRemove, onToggle }) => {
  const rowRenderer = useCallback(
    ({ index, key, style }) => {
      const todo = todos[index];
      return (
        <TodoListItem
          todo={todo}
          key={key}
          onRemove={onRemove}
          onToggle={onToggle}
          style={style}
        />
      );
    },
    [onRemove, onToggle, todos]
  );
  return (
    <List
      className="TodoList"
      width={512}
      height={513}
      rowCount={todos.length}
      rowHeight={57}
      rowRenderer={rowRenderer}
      list={todos}
      style={ outline: "none" }
    />
  );
};

export default memo(TodoList);
  • 최적화를 수행하기 전에, 각 항목의 실제 크기를 px 단위로 알아내야 한다.
  • List 컴포넌트를 사용하기 위해 함수를 새로 작성한다. 이 함수는 react-virtualized의 List 컴포넌트에서 각 아이템을 렌더링할 때 사용하며, 이 함수를 List 컴포넌트의 props로 설정해 주어야 한다.
  • 이 함수는 파라미터에 index, key, style 값을 객체 타입으로 받아 와서 사용한다.
  • List 컴포넌트를 사용할 때는 해당 리스트의 전체 크기와 각 항목의 높이, 각 항목을 렌더링할 때 사용해야 하는 함수, 그리고 배열을 props로 넣어줘야 한다.

immer

immer 라이브러리를 사용하면, 구조가 복잡한 객체도 매우 쉽고 짧은 코드를 사용하여 불변성을 유지하면서 업데이트해 줄 수 있다.

import produce from "immer";
const nextState = produce(originalState, (draft) => {
  draft.somewhere.deep.inside = 5;
});
  • produce 함수는 두 가지 파라미터를 받는다. 첫 번째 파라미터는 수정하고 싶은 상태이고, 두 번째 파라미터는 상태를 어떻게 업데이트할지 정의하는 함수이다.
  • 두 번째 파라미터로 전달되는 함수 내부에서 원하는 값을 변경하면, produce 함수가 불변성 유지를 대신해 주면서 새로운 상태를 생성해 준다.
  • 이 라이브러리의 핵심은 ‘불변성에 신경 쓰지 않는 것처럼 코드를 작성하되 불변성 관리는 제대로 해 주는 것’
  • immer를 사용하여 컴포넌트 상태를 작성할 때는 객체 안에 있는 값을 직접 수정하거나, 배열에 직접적인 변화를 일으키는 함수를 사용해도 무방하다.
  • 하지만 immer를 사용한다고 해서 무조건 코드가 간결해지지는 않기 때문에, 코드가 복잡할 때만 사용해도 충분하다.
  • produce 함수를 호출할 때, 첫 번째 파라미터가 함수 형태라면 업데이트 함수를 반환한다. 이러한 속성과 useState의 함수형 업데이트를 함께 활용하면 코드를 더욱 깔끔하게 만들 수 있다.




0420

엔터티와 인코딩

HTTP는 다음을 보장한다.

  • 객체는 올바르게 식별되므로(Content-Type 미디어 포맷과 Content-Language 헤더를 이용해서) 브라우저나 다른 클라이언트는 콘텐츠를 바르게 처리할 수 있다.
  • 객체는 올바르게 압축이 풀릴 것이다.(Content-Length와 Content-Encoding 헤더를 이용해서)
  • 객체는 항상 최신이다(엔터티 검사기와 캐시 만료 제어를 이용해서)
  • 사용자는 요구를 만족할 것이다(내용 협상을 위한 Accept 관련 헤더들에 기반해서)
  • 네트워크 사이를 빠르고 효율적으로 이동할 것이다(범위 요청, 델타 인코딩, 그 외의 데이터 압축을 이용해서)
  • 조작되지 않고 온전하게 도착할 것이다(전송 인코딩 헤더와 체크섬을 이용해서)

이 모든 것을 가능하게 하기 위해, HTTP는 콘텐츠를 나르기 위한 잘 라벨링된 엔터티를 사용한다.
HTTP 메시지를 인터넷 운송 시스템의 컨테이너라고 생각한다면, HTTP 엔터티는 메시지의 실질적인 화물이다.
엔터티 본문은 가공되지 않은 데이터만을 담고 있기에, 엔터티 헤더는 그 데이터의 의미에 대해 설명할 필요가 있다.

Content-Length: 엔터티의 길이

Content-Length 헤더는 메시지의 엔터티 본문의 크기를 바이트 단위로 나타낸다.
어떻게 인코딩 되었든 상관없이 크기를 표현할 수 있다.
Content-Length 헤더는 메시지를 청크 인코딩으로 전송하지 않는 이상, 엔터티 본문을 포함한 메시지에서는 필수적으로 있어야 한다.
서버 충돌로 인해 메시지가 잘렸는지 감지하고자 할 때와 지속 커넥션을 공유하는 메시지를 올바르게 분할하고자 할 때 필요하다.

잘림 검출

Content-Length가 없다면 클라이언트는 커넥션이 정상적으로 닫힌 것인지 메시지 전송 중에 서버에 충돌이 발생한 것인지 구분하지 못한다.
메시지 잘림은 캐싱 프락시 서버에서 특히 취약하다. 만약 캐시가 잘린 메시지를 수신했으나 잘렸다는 것을 인식하지 못했다면, 캐시는 결함이 있는 콘텐츠를 저장하고 계속해서 제공하게 될 것이다.

잘못된 Content-Length

Content-Length가 잘못된 값을 담고 있을 경우 아예 빠진 것보다도 큰 피해를 유발할 수 있다.
초창기 클라이언트들과 서버들 중 일부는 Content-Length의 계산과 관련된 버그들을 가지고 있기 때문에, 오동작을 했는지 탐지하고 교정을 시도한다.

Content-Length와 지속 커넥션

Content-Length는 지속 커넥션을 하기 위해 필수다.
Content-Length 헤더는 클라이언트에게 메시지 하나가 어디서 끝나고 다음 시작은 어디인지 알려준다.
커넥션이 지속적이기 때문에, 클라이언트가 커넥션이 닫힌 위치를 근거로 메시지의 끝을 인식하는 것은 불가능하다.

다만 청크 인코딩을 사용할 때는 Content-Length 헤더 없는 지속 커넥션이 가능하다.

콘텐츠 인코딩

HTTP는 보안을 강화하거나 압축을 통해 공간을 절약할 수 있도록, 엔터티 본문을 인코딩할 수 있게 해준다.
만약 본문의 콘텐츠가 인코딩되어 있다면, Content-Length 헤더는 인코딩된 본문의 길이를 바이트 단위로 정의한다.

엔터티 본문 길이 판별을 위한 규칙

  1. 본문을 갖는 것이 허용되지 않는 HTTP 메시지에는, 본문 계산을 위한 Content-Length 헤더가 무시된다.
  2. 메시지가 Transfer-Encoding 헤더를 포함하고 있다면, 메시지가 커넥션이 닫혀서 먼저 끝나지 않는 이상 엔터티는 0바이트 청크라 불리는 특별한 패턴으로 끝나야 한다.
  3. Content-Length 헤더를 갖는다면 본문의 길이를 담게 된다.
  4. 메시지가 ‘multipart/byteranges’ 미디어 타입을 사용하고 엔터티 길이가 별도로 정의되지 않았다면, 멀티파트 메시지의 각 부분은 각자 스스로의 크기를 정의할 것이다. 이 멀티파트 유형은 스스로 크기를 결정할 수 있는 유일한 엔터티 본문 유형이다.
  5. 위의 어떤 규칙에도 해당되지 않는다면, 엔터티는 커넥션이 닫힐 때 끝난다.

엔터티 요약

엔터티 본문 데이터에 대한 의도하지 않은 변경을 감지하기 위해, 최초 엔터티가 생성될 때 송신자는 데이터에 대한 체크섬을 생성할 수 있으며, 수신자는 의도하지 않은 엔터티의 변경을 잡아내기 위해 그 체크섬으로 기본적인 검사를 할 수 있다.

Content-MD5 헤더는 서버가 엔터티 본문에 MD5 알고리즘을 적용한 결과를 보내기 위해 사용된다.
응답을 처음 만든 서버만이 Content-MD5 헤더를 계산해서 보내고, 중간에 있는 프락시와 캐시는 그 헤더를 변경하거나 추가하지 않는다.

미디어 타입과 차셋(Charset)

Content-Type 헤더 필드는 엔터티 본문의 MIME 타입을 기술한다.
MIME 타입은 전달되는 데이터 매체의 기저 형식의 표준화된 이름이다.

텍스트 매체를 위한 문자 인코딩

charset 매개변수를 지원해 더 자세한 내용 유형을 지정할 수 있다.

멀티파트 미디어 타입

MIME 멀티파트 이메일 메시지는 서로 붙어있는 여러 개의 메시지를 포함하며, 하나의 복합 메시지로 보내진다.
HTTP는 멀티파트 본문도 지원한다. 그러나 일반적으로는 폼을 채워서 제출할 때와 문서의 일부분을 실어 나르는 범위 응답을 할 때의 두 가지 경우에만 사용된다.

멀티파트 폼 제출

HTTP 폼을 채워서 제출하면, 가변 길이 텍스트 필드와 업로드 될 객체는 각각이 멀티파트 본문을 구성하는 하나의 파트가 되어 보내진다.
멀티파트 본문은 여러 다른 종류와 길이의 값으로 채워진 폼을 허용한다.

멀티파트 범위 응답

범위 요청에 대한 HTTP 응답 또한 멀티파트가 될 수도 있다.

콘텐츠 인코딩

콘텐츠 인코딩 과정

  1. 웹 서버가 원본 Content-Type과 Content-Length 헤더를 수반한 원본 응답 메시지를 생성한다.
  2. 콘텐츠 인코딩 서버가 인코딩 메시지를 생성한다. 콘텐츠 인코딩 서버는 Content-Encoding 헤더를 추가하여, 수신 측 애플리케이션이 그것을 디코딩할 수 있도록 한다.
  3. 수신 측 프로그램은 인코딩된 메시지를 받아서 디코딩하고 원본을 얻는다.

콘텐츠 인코딩 유형

gzip 인코딩은 전송되는 메시지의 크기를 정보의 손실 없이 줄이기 위한 무손실 압축 알고리즘이다. gzip은 일반적으로 가장 효율적이고 가장 널리 쓰이는 압축 알고리즘이다.

Accept-Encoding 헤더

서버에서 클라이언트가 지원하지 않는 인코딩을 사용하는 것을 막기 위해, 클라이언트는 자신이 지원하는 인코딩의 목록을 Accept-Encoding 요청 헤더를 통해 전달한다.
만약 이것이 포함되지 않았다면, 서버는 클라이언트가 어떤 인코딩이든 받아들일 수 있는 것으로 간주한다.

클라이언트는 각 인코딩에 Q 값을 매개변수로 더해 선호도를 나타낼 수 있다.
1.0에 가까울수록 인코딩을 원한다는 뜻이다.

전송 인코딩과 청크 인코딩

메시지 데이터가 네트워크를 통해 전송되는 방법을 바꾸기 위해 전송 인코딩을 메시지에 적용할 수 있다.

안전한 전송

HTTP에서 전송된 메시지의 본문이 문제를 일으킬 수 있는 경우는 몇 가지 밖에 없다.

알 수 없는 크기
몇몇 콘텐츠 인코더는 콘텐츠를 먼저 생성하지 않고서는 메시지 본문의 최종 크기를 판단할 수 없다.
이 서버들은 그 사이즈를 알기 전에 데이터의 전송을 시작하려고 한다.
HTTP는 데이터에 앞서 Content-Length를 요구하기 때문에, 몇몇 서버는 종결 꼬리말을 포함시켜 전송 인코딩으로 데이터를 보내려 시도한다.

보안
SSL과 같은 유명 전송 계층 보안 방식이 있기 때문에 전송 인코딩 보안은 흔하지 않다.

청크 인코딩

청크 인코딩은 메시지를 일정 크기의 청크 여럿으로 쪼갠다.
청크 인코딩을 이용하면메시지를 보내기 전에 전체 크기를 알 필요가 없어진다.
본문이 동적으로 생성됨에 따라, 서버는 그중 일부를 버퍼에 담은 뒤 그 청크를 그것의 크기와 함께 보낼 수 있다.

지속 커넥션에서는 본문을 쓰기 전에 반드시 Content-Length 헤더에 본문의 길이를 담아서 보내줘야 한다. 콘텐츠가 서버에서 동적으로 생성되는 경우에는, 보내기 전에 본문의 길이를 알아내는 것이 불가능할 것이다.
청크 인코딩은 동적으로 본문이 생성되면서, 서버는 그중 일부를 버퍼에 담은 뒤 그 한덩어리를 그의 크기와 함께 보낼 수 있다.
본문을 모두 보내면 크기가 0인 청크로 본문이 끝났음을 알리고 다음 응답을 위해 커넥션을 열린 채로 유지할 수 있다.

콘텐츠와 전송 인코딩의 조합

콘텐츠 인코딩과 전송 인코딩은 동시에 사용될 수 있다.
콘텐츠 인코딩 -> 전송 인코딩 순으로 전송된다.

전송 인코딩 규칙

  • 전송 인코딩의 집합은 반드시 ‘chunked’를 포함해야 한다. 유일한 예외는 메시지가 커넥션의 종료로 끝나는 경우 뿐이다.
  • 청크 전송 인코딩이 사용되었다면, 메시지 본문에 적용된 마지막 전송 인코딩이 존재해야 한다.
  • 청크 전송 인코딩은 반드시 메시지 본문에 한 번 이상 적용되어야 한다.

시간에 따라 바뀌는 인스턴스

웹 객체는 정적이지 않고 시간에 따라 다른 버전의 객체를 가리킬 수 있다.
HTTP 프로토콜은 어떤 특정한 종류의 요청이나 응답을 다루는 방법들을 정의하는데, 이것은 인스턴스 조작이라 불리며 객체의 인스턴스에 작용한다.

이들 중 대표적인 두 가지가 범위 요청과 델타 인코딩이다. 이 둘 모두가, 클라이언트가 자신이 갖고 있는 리소스의 사본이 서버가 갖고 있는 것과 정확히 같은지 판단하고, 상황에 따라서는 새 인스턴스를 요청할 수 있는 능력을 가질 것을 요구한다.

검사기와 신선도

신선도

서버는 클라이언트에게 얼마나 오랫동안 콘텐츠를 캐시하고 그것이 신선하다고 가정할 수 있는지에 대한 정보를 줄 것이다.
서버는 Expires나 Cache-Control 헤더를 통해 이러한 정보를 제공할 수 있다.

Expires는 만료일을 지정한다. Cache-Control 헤더는 문서의 최대 수명을 문서가 서버를 떠난 후로부터의 총 시간을 초 단위로 정한다.

조건부 요청과 검사기

HTTP는 클라이언트에게 리소스가 바뀐 경우에만 사본을 요청하는 조건부 요청이라 불리는 특별한 요청을 할 수 있는 방법을 제공한다.
조건부 요청은 평범한 요청 메시지이지만, 특정 조건이 참일 때만 수행된다.
조건부 요청은 ‘If-‘ 로 시작하는 조건부 헤더에 의해 구현된다.

각 조건부 요청은 특정 검사기 위에서 동작한다. 검사기는 문서의 테스트된 특정 속성이다.
일련번호나 버전 번호 혹은 문서의 최종 변경일과 같은 검사기를 생각해볼 수 있다.
Last-Modified와 ETag는 HTTP에 의해 사용되는 두 개의 주요한 검사기다.

HTTP는 검사기를 약한 검사기와 강한 검사기 두 가지로 분류한다.

약한 검사기는 리소스의 인스턴스를 고유하게 식별하지 못하는 경우도 있다.
ex) 객체의 바이트 단위 크기, 최종 변경 시각

강한 검사기는 언제나 고유하게 식별한다.
ex) 암호 체크섬, ETag

범위 요청

HTTP는 클라이언트가 문서의 일부분이나 특정 범위만 요청할 수 있도록 해 준다.
범위 요청을 이용하면, HTTP 클라이언트는 받다가 실패한 엔터티를 일부 혹은 범위로 요청함으로써 다운로드를 중단된 시점에서 재개할 수 있다.
클라이언트의 범위 요청은 오직 클라이언트와 서버가 같은 버전의 문서를 갖고 있을 때만 의미가 있다.

델타 인코딩

새 페이지 전체를 보내는 대신, 페이지에 대한 클라이언트의 사본에 대해 변경된 부분만을 서버가 보낸다면 클라이언트는 더 빨리 페이지를 얻을 수 있을 것이다.
델타 인코딩은 객체 전체가 아닌 변경된 부분에 대해서만 통신하여 전송량을 최적화하는, HTTP 프로토콜의 확장이다.

이는 전송 시간을 줄일 수 있지만 구현하기가 까다로울 수 있다. 문서를 제공하는데 걸리는 시간이 줄어드는 대신, 서버는 문서의 과거 사본을 모두 유지하기 위해 디스크 공간을 더 늘려야 한다. 이는 전송량 감소로 얻은 이득을 무의미하게 만들 것이다.


reference
HTTP 완벽 가이드




0422

리액트 라우터

설치

yarn add react-router-dom

라우터 연결

프로젝트에 리액트 라우터를 적용할 때는 index.js 파일에서 react-router-dom에 내장되어 있는 BrowserRouter 컴포넌트를 사용하여 감싸면 된다.
이 컴포넌트는 웹 애플리케이션에 HTML5의 History API를 사용하여 페이지를 새로고침하지 않고도 주소를 변경하고, 현재 주소에 관련된 정보를 props로 쉽게 조회하거나 사용할 수 있도록 해 준다.

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

라우트 구성

Routes, Route 컴포넌트를 사용해 경로에 따라 페이지 엘리먼트를 렌더링 하도록 구성한다.
Route 컴포넌트는 사용자의 현재 경로에 따라 다른 컴포넌트를 보여 준다.

const App = () => {
  return (
    <div>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </div>
  );
};

useRoutes 훅을 사용하는 방법도 Routes 컴포넌트를 사용해 라우트를 구성하는 것과 기능적으로 동일하다.
다만, JS 객체를 사용해 라우트를 구성할 수 있다.

import { useRoutes } from "react-router-dom";

const App = () => {
  const routeElements = useRoutes([
    { path: "/", element: <Home /> },
    { path: "/about", element: <About /> },
  ]);

  return <div>{routeElements}</div>;
};

Link 컴포넌트는 클릭하면 다른 주소로 이동시켜 주는 컴포넌트이다.
페이지를 새로 불러오지 않고 애플리케이션은 그대로 유지한 상태에서 History API를 사용하여 페이지의 주소만 변경해 준다.

const App = () => {
  const routeElements = useRoutes([
    { path: "/", element: <Home /> },
    { path: "/about", element: <About /> },
  ]);

  return (
    <div>
      <ul>
        <li>
          <Link to="/"></Link>
        </li>
        <li>
          <Link to="/about">소개</Link>
        </li>
      </ul>
      <hr />
      {routeElements}
    </div>
  );
};

URL 파라미터와 쿼리

  • 파라미터 예시: /profile/junghoon
  • 쿼리 예시: /about?details=true

유동적인 값을 사용해야 하는 상황에서 파라미터나 쿼리 중 무엇을 써야하는 지에 대한 무조건적인 규칙은 없다.
일반적으로 파라미터는 특정 아이디 혹은 이름을 사용하여 조회할 때 사용하고, 쿼리는 어떤 키워드를 검색하거나 페이지에 필요한 옵션을 전달할 때 사용한다.

URL 파라미터

URL 파라미터를 사용할 때는 라우트로 사용되는 컴포넌트에서 useParams() 훅을 이용해 params 값을 참조한다.
match 객체는 v6부터 더 이상 지원하지 않는다.
그리고 상위 컴포넌트에서 컴포넌트를 위한 라우트를 정의한다. 예를 들어, /profile/:username이라고 넣어 주면 된다.

const data = {
  velopert: {
    name: "이정훈",
    description: "리액트를 좋아하는 개발자",
  },
  gildong: {
    name: "홍길동",
    description: "고전 소설 홍길동전의 주인공",
  },
};

const Profile = () => {
  const { username } = useParams();

  const profile = data[username];

  if (!profile) {
    return <div>존재하지 않는 사용자입니다.</div>;
  }
  return (
    <div>
      <h3>
        {username}({profile.name})
      </h3>
      <p>{profile.description}</p>
    </div>
  );
};

URL 쿼리

쿼리는 location 객체에 들어 있는 search 값에서 조회할 수 있다.
v6부터는 location 객체가 제공되지 않고, useLocation() 훅을 사용해 search를 조회할 수 있다.
쿼리 문자열을 객체로 변환할 때는 qs라는 라이브러리를 사용한다.

URL 쿼리를 설정하거나, 가져올 때 useSearchParams()훅을 사용할 수도 있다.

const About = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  console.log(searchParams.get("a"));
  // 콘솔에는 a를 키로 한 값이 결과로 나온다.

  return (
    <div>
      <h1>소개</h1>
      <p>이 프로젝트는 리액트 라우터 기초를 실습해 보는 예제 프로젝트입니다.</p>
    </div>
  );
};

네스트(중첩) 라우팅

라우트 내부에 중첩된 하위 라우팅 설정을 진행한다.
중첩된 라우팅 설정은 상위 Route 컴포넌트 내부에 중첩된 Route 컴포넌트로 구성한다.

<Routes>
  {/* exact prop은 기본 값이므로 명시적으로 설정하지 않아도 됩니다. */}
  {/* children 대신, `element` prop을 사용합니다. */}
  <Route path="/" element={<Home />} />
  {/* 경로에 포함된 *는 하위에 중첩된 경로가 있음을 나타냅니다. */}
  <Route path="users/*" element={<Users />} />
    {/* users/me 중첩 경로 */}
    <Route path="me" element={<OwnUserProfile />} />
    {/* users/:id 중첩 경로 */}
    <Route path=":id" element={<UserProfile />} />
  </Route>
</Routes>;
  • element prop을 사용하면 중첩 라우트 구성을 분산시키지 않고 한 곳에서 관리할 수 있어 편리하다.
  • 중첩 라우트는 상위 경로에 상대적인 경로가 사용된다.

라우트 정보

v5 버전까지는 라우트 컴포넌트에 라우트 정보(history, location, match)가 제공됐지만, v6 버전부터는 제공되지 않는다.

useNavagation

v5 버전의 useHistory 대신 v6 버전에서는 useNavigate 훅을 사용해 페이지 탐색이 가능하다.
이를 통해 컴포넌트 내에 구현하는 메서드에서 라우터 API를 호출할 수 있다.
예를 들어 특정 버튼을 눌렀을 때 뒤로 가거나, 로그인 후 화면을 전환하거나, 다른 페이지로 이탈하는 것을 방지할 때 활용한다.

const HistorySample = () => {
  const navigate = useNavigate();

  const handleGoBack = () => navigate(-1);
  const handleGoHome = () => navigate("/");

  return (
    <div>
      <button onClick={handleGoBack}>뒤로</button>
      <button onClick={handleGoHome}>홈으로</button>
    </div>
  );
};
  • navigate 함수에 -1을 전달하면 뒤로 가기와 동일하다.
  • 두 번째 인자로 replace나 state 값을 선택적으로 넘길 수 있다. replace를 true로 하면 history에 남지 않는다.

useLocation

useLocation 훅은 Location 객체를 반환한다. 이 객체는 URL 경로가 업데이트 될 때 마다 사이드 이펙트를 수행하고자 할 때 유용하게 활용할 수 있다.

import { useEffect } from "react";
import { useLocation } from "react-router-dom";

function App() {
  let location = useLocation();

  useEffect(() => {
    // ... 사이드 이펙트
  }, [location]);
}
  • URL 경로 문자를 가리키는 pathname, 쿼리를 가리키는 search, state 등의 프로퍼티에 접근 가능하다.

useMatch

useMatch 훅은 현재 URL 기준으로 설정된 경로와 일치하는 데이터를 반환한다.

const { pathname } = useLocation();
const match = useMatch(pathname); // { params, pathname, pathnameBase, pattern }

NavLink는 Link와 비슷하다. 현재 경로와 Link에서 사용하는 경로가 일치하는 경우 특정 스타일 혹은 CSS 클래스를 적용할 수 있는 컴포넌트이다.

v5의 activeClassName, activeStyle prop이 제거되었다.
대신 className, style prop에 콜백함수를 설정하여 활성 상태에 따라 스타일을 변경할 수 있다.

const Profiles = () => {
  const activeStyle = {
    background: "black",
    color: "white",
  };

  return (
    <div>
      <h3>사용자 목록:</h3>
      <ul>
        <li>
          <NavLink
            to="/profiles/velopert"
            style={({ isActive }) => (isActive ? activeStyle : {})}
          >
            velopert 프로필
          </NavLink>
        </li>
        <li>
          <NavLink
            to="/profiles/gildong"
            style={({ isActive }) => (isActive ? activeStyle : {})}
          >
            gildong 프로필
          </NavLink>
        </li>
      </ul>
      <Routes>
        <Route path="/" element={<div>사용자를 선택해 주세요.</div>} />
        <Route path="/:username" element={<Profile />} />
      </Routes>
    </div>
  );
};

태그:

카테고리:

업데이트: