0314 - 0320

0314

타입 주변에 null 값 배치하기

strictNullChecks 설정을 하면 null이나 undefined 값 관련된 오류들이 나타나게 된다.
이 때, 값이 전부 null이거나 전부 null이 아닌 경우로 분명히 구분된다면, 값이 섞여 있을 때보다 다루기 쉽다.

function extent(nums: number[]) {
  let min, max;

  for (const num of nums) {
    if (!min) {
      min = num;
      max = num;
    } else {
      min = Math.min(min, num);
      max = Math.max(max, num);
    }
  }
  return [min, max];
}
  • 예를 들어, 이 코드에는 버그와 함께 설계적 결함이 존재한다.
  • 최솟값이나 최댓값이 0인 경우, 값이 덧씌워져 버린다. 예를 들어, extent([0, 1, 2])의 결과는 [1, 2]가 된다.
  • nums 배열이 비어 있다면 함수는 [undefined, undefined]를 반환한다.
  • undefined를 포함하는 객체는 다루기 어렵고 권장하지 않는다. 코드를 살펴보면 min과 max가 동시에 undefined이거나 둘다 아니라는 것을 알 수 있지만, 이러한 정보는 타입 시스템에서 표현할 수 없다.
  • strictNullChecks 설정을 하면 extent의 반환 타입이 (number | undefined)[]로 추론되어서 설계적 결함이 분명해진다.
  • 이에 대한 적절한 해법은 min과 max를 한 객체 안에 넣고 null이거나 null이 아니게 하면 된다.
function extent(nums: number[]) {
  let result: [number, number] | null = null;

  for (const num of nums) {
    if (!result) {
      result = [num, num];
    } else {
      result = [Math.min(result[0], num), Math.max(result[1], num)];
    }
  }
  return result;
}
  • 이제는 반환 타입이 [number, number] | null이 되어 사용하기 수월해졌다.
  • null 아님 단언을 사용하면 min과 max를 얻을 수 있다.
  • extent 결과값으로 단일 객체를 사용함으로써 설계를 개선했고, 타입스크립트가 null 값 사이의 관계를 이해할 수 있도록 했으며 버그를 제거했다.

클래스 작성 시

클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 하는 것이 좋다.

class UserPosts {
  user: UserInfo;
  posts: Post[];

  constructor(user: UserInfo, posts: Post[]) {
    this.user = user;
    this.posts = posts;
  }

  static async init(userId: string): Promise<UserPosts> {
    const [user, posts] = await Promise.all([
      fetchUser(userId),
      fetchPostsForUser(userId),
    ]);

    return new UserPosts(user, posts);
  }
}
  • 이와 같이 코드를 작성하면 클래스는 완전히 null이 아니게 되고, 메서드를 작성하기 쉬워진다.
  • null인 경우가 필요한 속성은 프로미스로 바꾸면 안 된다. 코드가 매우 복잡해지며 모든 메서드가 비동기로 바뀌어야 한다.

유니온의 인터페이스보다는 인터페이스의 유니온 사용하기

유니온 타입의 속성을 가지는 인터페이스를 작성 중이라면, 인터페이스의 유니온 타입을 사용하는 게 더 알맞지 않을지 검토해 봐야 한다.

interface Layer {
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint | PointPaint;
}
  • layout 속성은 모양이 그려지는 방법과 위치를 제어하고, paint 속성은 스타일을 제어한다.
  • layout이 LineLayout 타입이면서 paint 속성이 FillPaint 타입인 것은 무효한 상태이다.
  • 더 나은 방법으로 모델링하려면 각각 타입의 계층을 분리된 인터페이스로 둬야 한다.
interface FillLayer {
  layout: FillLayout;
  paint: FillPaint;
}

interface LineLayer {
  layout: LineLayout;
  paint: LinePaint;
}

interface PointLayer {
  layout: PointLayout;
  paint: PointPaint;
}

type Layer = FillLayer | LineLayer | PointLayer;
  • 이런 형태로 Layer를 정의하면 layout과 paint 속성이 잘못된 조합으로 섞이는 경우를 방지할 수 있다.
  • 이러한 패턴의 가장 일반적인 예시는 태그된 유니온이다.
  • type 속성을 태그로 두어 태그를 참고하여 Layer의 타입 범위를 좁힐 수 있다.
  • 어떤 데이터 타입을 태그된 유니온으로 표현할 수 있다면, 보통 그렇게 하는 것이 좋다.

여러 개의 선택적 필드가 동시에 값이 있거나, 없어야 하는 경우

interface Person {
  name: string;
  // 다음은 둘 다 동시에 있거나 동시에 없다.
  placeOfBirth?: string;
  dateOfBirth?: Date;
}
  • 타입 정보를 담고 있는 주석은 문제가 될 소지가 매우 높다.
  • 두 선택적 필드는 실제로 관련되어 있지만, 타입 정보에는 어떤 관계도 표현되지 않았다.
  • 두 개의 속성을 하나의 객체로 모으는 것이 더 좋은 설계이다.
interface Person {
  name: string;
  birth?: {
    placeOfBirth: string;
    dateOfBirth: Date;
  };
}
  • 이제 둘 중 하나만 있는 경우 오류가 발생한다.

타입의 구조를 손 댈 수 없는 상황 (ex - API 결과)

앞서 다룬 인터페이스의 유니온을 사용해서 속성 사이의 관계를 모델링 할 수 있다.

interface Name {
  name: string;
}

interface PersonWithBirth extends Name {
  placeOfBirth: string;
  dateOfBirth: Date;
}

type Person = Name | PersonWithBirth;
  • 유니온 타입의 속성을 여러 개 가지는 인터페이스에서는 속성 간의 관계가 분명하지 않기 때문에 실수가 자주 발생하므로 주의해야 한다.
  • 유니온의 인터페이스보다 인터페이스의 유니온이 더 정확하고 타입스크립트가 이해하기도 좋다.
  • 태그된 유니온은 타입스크립트와 매우 잘 맞기 때문에 자주 볼 수 있는 패턴이다.

string 타입보다 더 구체적인 타입 사용하기

string 타입의 범위는 매우 넓기에 string 타입으로 변수를 선언하려 한다면, 그보다 더 좁은 타입이 적절하지는 않을지 검토해 보어야 한다.

값을 몇 개의 고정값으로만 받을 경우

예를 들어 Album 인터페이스에서 recordingType을 ‘live’와 ‘studio’ 두 개의 값으로 유니온 타입을 정의해야 한다고 가정해보자.
이때 enum을 사용할 수도 있지만 일반적으로 추천하지 않는다고 한다.

type RecordingType = "studio" | "live";

interface Album {
  artist: string;
  title: string;
  releaseDate: Date;
  recordingType: RecordingType;
}
  • 이와 같이 코드를 작성하면 타입스크립트는 오류를 더 세밀하게 체크한다.
  • 이러한 방식에는 세 가지 장점이 더 있다.
  1. 타입을 명시적으로 정의함으로써 다른 곳으로 값이 전달되어도 타입 정보가 유지된다.
  2. 타입을 명시적으로 정의하고 해당 타입의 의미를 설명하는 주석을 붙여 넣을 수 있다.
  3. keyof 연산자로 더욱 세밀하게 객체의 속성 체크가 가능해진다.
/** 주석 내용 */
type RecordingType = "studio" | "live";

객체의 속성 이름을 함수 매개변수로 받을 때

어떤 배열에서 한 필드의 값만 추출하는 함수를 작성한다고 가정해보자.

function pluck<T>(records: T[], key: keyof T): T[keyof T][] {
  return records.map((r) => r[key]);
}
  • 제너릭과 keyof를 활용하여 위와 같이 코드를 작성했다.
  • 그런데 key의 값으로 하나의 문자열을 넣게 되면, 그 범위가 너무 넓어서 적절한 타입이라고 보기 어렵다.
  • 예를들어 releaseDate의 타입은 (string | Date)[]가 아니라 Date[]이어야 한다. 이는 string에 비해 훨씬 범위가 좁기는 하지만 그래도 여전히 넓다.
  • 범위를 더 좁히기 위해 keyof T의 부분집합으로 두 번째 제너릭 매개변수를 도입해야 한다.
function pluck<T, K extends keyof T>(records: T[], key: K): T[K][] {
  return records.map((r) => r[key]);
}

이제 타입 시그니처가 완벽해지고, 매개변수 타입이 정밀해진 덕에 언어 서비스는 키에 자동 완성 기능을 제공할 수 있게 해 준다.

부정확한 타입보다는 미완성 타입을 사용하기

일반적으로는 타입이 구체적일수록 버그를 더 많이 잡고 타입스크립트가 제공하는 도구를 활용할 수 있게 된다.
그러나 타입 선언의 정밀도를 높이는 일에는 주의를 기울여야 한다. 실수가 발생하기 쉽고 잘못된 타입은 차라리 없는 것보다 못할 수 있다.
타입 선언을 세밀하게 만들 때 너무 과하게 하는 경우 오히려 타입이 부정확해져서 사용자들에게 불편함을 줄 수 있다.

일반적으로 복잡한 코드는 더 많은 테스트가 필요하고 타입의 관점에서도 마찬가지이다.
타입을 정제할 때 any 같은 매우 추상적인 타입은 정제하는 것이 좋다. 그러나 타입이 구체적으로 정제된다고 해서 정확도가 무조건 올라가지는 않는다.

  • 정확하게 타입을 모델링할 수 없다면, 부정확하게 모델링하지 말아야 한다. 또한 any와 unknown을 구별해서 사용해야 한다.
    • any는 타입 검사를 느슨하게 하므로 추후에 개발 후 예기치 못한 문제가 발생할 가능성이 매우 높다.
    • unknown은 any 타입과는 다르게 프로퍼티 또는 연산을 하는 경우 컴파일러가 체크하여 문제 되는 코드를 미리 예방할 수 있다.
  • 타입 정보를 구체적으로 만들수록 오류 메시지와 자동 완성 기능에 주의를 기울여야 한다. 정확도뿐만 아니라 개발 경험과도 관련된다.

데이터가 아닌, API와 명세를 보고 타입 만들기

  • 코드의 구석 구석까지 타입 안정성을 얻기 위해 API 또는 데이터 형식에 대한 타입 생성을 고려해야 한다.
  • 데이터에 드러나지 않는 예외적인 경우들이 문제가 될 수 있기 때문에 데이터보다는 명세로부터 코드를 생성하는 것이 좋다.
  • 명세를 기반으로 타입을 작성한다면 현재까지 경험한 데이터뿐만 아니라 사용 가능한 모든 값에 대해서 작동한다는 확신을 가질 수 있다.
  • API의 명세로부터 타입을 생성할 수 있다면 그렇게 하는 것이 좋다. 특히 GraphQL처럼 자체적으로 타입이 정의된 API에서 잘 동작한다.




0315

styled-components

CSS 스타일을 추상화 한 javascript 객체 대신, CSS 스타일 문법을 그대로 사용할 수 있는 React 스타일 컴포넌트로 활용할 수 있다.
React 컴포넌트 시스템에서 CSS 스타일링을 효율적으로 작성화기 위한 방법

장점

  • 손쉬운 유지 보수
    • 컴포넌트에 영향을 미치는 스타일을 찾기 위해 여러 파일을 검색 할 필요가 없으므로 코드 베이스의 크기와 관계없이 유지 보수가 용이
  • 고유한 CSS 클래스 생성
    • 스타일 컴포넌트를 추적해 CSS로 작성된 백틱 스타일을 고유한 CSS 클래스 이름으로 변경한다.
    • 스타일 컴포넌트에 의해 자동 생성된 고유 CSS 클래스 이름은 sc- 접두사로 시작된다.
  • 벤더 프리픽스 자동 설정
    • CSS 표준 문법만 사용하면 자동으로 처리한다.
  • 동적 스타일링
    • props 또는 theme 속성을 사용해 컴포넌트 외부에서 스타일을 관리하는 것은, 수십 개의 CSS 클래스를 손수 관리할 필요가 없다.
    • 컴포넌트 외부에서 손쉽게 동적으로 스타일을 관리할 수 있다.

설치

npm i styled-components

사용법

React 프로젝트에서 styled-components 모듈에서 styled를 불러온 후, HTML 표준 컴포넌트 이름을 추가한 다음 백틱 기호로 감싼 영역에 CSS 코드를 작성하면 CSS 스타일이 반영된 React 컴포넌트를 만들어 낼 수 있다.

import styled from "styled-components";

export const SectionHeader = styled.h2`
  color: #06f;
  font-size: 1.45rem;
`;

// 위의 코드와 같음
export const SectionHeader = styled("h2")`
  color: #06f;
  font-size: 1.45rem;
`;

props 설정

React 컴포넌트는 요소로 사용될 때 속성을 전달 받고, props 속성으로 전달 받은 속성에 접근이 가능하다.
Styled 컴포넌트 또한 props 속성을 인터폴레이션 내부에 설정된 함수를 통해 처리할 수 있다.

import styled from "styled-components";

const Button = styled.button`
  color: ${(props) => (props.reject ? "#f60" : "#06f")};
`;

export const StyleTest = () => {
  return (
    <div>
      <Button>승인</Button>
      <Button reject>거절</Button>
    </div>
  );
};

스타일 확장

styled 함수에 스타일드 컴포넌트를 사용해 정의된 컴포넌트를 전달하면 컴포넌트 스타일을 확장할 수 있다.
Button 컴포넌트 스타일을 확장한 FillButton 컴포넌트 스타일을 만들어보자

const Button = styled.button`
  color: ${(props) => (props.reject ? "#f60" : "#06f")};
`;

const FillButton = styled(Button)`
  border: 0;
  padding: 0.45em 0.95em 0.6em;
  background-color: ${(props) => (props.reject ? "#026" : "#06f")};
  color: ${(props) => (props.reject ? "#09f" : "#fff")};
  font-weight: 600;
`;

이렇게 스타일을 확장한 경우 기존 스타일이 중첩되게 되면 덮어써지게 된다.

React 컴포넌트 스타일 확장

일반 React 컴포넌트 또한 스타일 확장이 가능하다.
className 속성을 전달 받도록 설정해야 스타일 확장이 적용된다.

// 이와 같이 className 속성 전달 받도록 설정
const Link = ({ className, to, mode, label, children }) => {
  return (
    <a href={to} className={className}>
      {children}
    </a>
  );
};

// props 사용 시, 디스트럭처링 문법 사용 가능
const IndicatorLink = styled(Link)`
  padding: 0.8em;
  font: 16px/1 Verdana;
  border-radius: ${(props) => props.radius ?? 0};
  border: ${({ border }) => border ?? "3px solid #06f"};
  color: ${({ primary }) => primary ?? "#06f"};
`;

// React 컴포넌트 스타일 확장한 컴포넌트 사용 예시
export const StyleTest = () => {
  return (
    <IndicatorLink
      to="https://www.naver.com"
      radius="10px"
      border="6px double hsla(220, 99%, 50%, 0.68)"
    >
      hihi
    </IndicatorLink>
  );
};

스타일 래퍼

컴포넌트, 스타일 컴포넌트를 구분하지 않고 내부에 스타일 컴포넌트를 정의하는 것이 관리하기 편하다.

// 스타일 컴포넌트
const Container = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  font: 14px/1 Verdana;
  color: ${({ primary }) => primary ?? "hsla(220, 99%, 50%, 0.89)"};
`;

// 컴포넌트
export const Counter = ({ primary, children }) => (
  <Container primary={primary}>{children}</Container>
);
  • 컴포넌트 내부에 포함한 스타일 컴포넌트가 많아질 경우, 별도 파일로 분리 관리하는 것이 좋다.

다만, 아래와 같이 컴포넌트 내부에 스타일 컴포넌트를 정의해 사용하면 컴포넌트를 렌더링 할 때마다 스타일 컴포넌트를 다시 정의하므로 애플리케이션 속도에 큰 영향을 미친다. 이처럼 사용하면 안 된다.

export const Counter = ({ primary, children }) => {
  const Container = styled.div`
    display: flex;
    align-items: center;
    justify-content: center;
    font: 14px/1 Verdana;
    color: ${({ primary }) => primary ?? "hsla(220, 99%, 50%, 0.89)"};
  `;

  return <Container primary={primary}>{children}</Container>;
};

중첩 규칙

styled-components는 Sass처럼 중첩 규칙, 가상 클래스/요소를 지원한다.

const Container = styled.div`
  border: 1px solid black;

  output {
    user-select: none;
    font-size: 40px;
  }
`;

// 컴포넌트
export const Counter = () => (
  <Container>
    <output>9</output>
  </Container>
);

가상 클래스

스타일드 컴포넌트의 CSS 코드 안에 가상 클래스를 설정하면 스타일이 적용된다.

// 스타일 컴포넌트
const ControlButton = styled.button`
  &:hover,
  &:focus {
    color: #036;
    font-weight: 400;
  }
`;

// 컴포넌트
export const Counter = () => <ControlButton>button</ControlButton>;

가상 요소

CSS 코드 안에 가상 요소를 설정하면 스타일이 적용된다.

const ControlButton = styled.button`
  &.increase {
    &::before {
      content: "📤";
    }
  }
`;

export const Counter = () => (
  <ControlButton className="increase">button</ControlButton>
);

&.increase::before 가상 요소는 <Counterbutton className="increase"> 내부에 생성된다.

추가 props 첨부

styled-components의 attrs 생성자를 사용하면 정적, 동적 props를 정의할 수 있다.
이 방법을 활용하면 CSS 속성 값이 javascript 식을 장황하게 나열하지 않고, 한 곳에서 깔끔하게 관리할 수 있다.

const AppInput = styled.input.attrs(({ size, primary }) => ({
  // 정적 props
  type: "text",
  className: "appInput",

  // 동적 props 정의
  color: primary ?? "#06f",
  margin: size ?? "1em",
  padding: size ?? "1em",
}))`
  border: 2px solid ${({ color }) => color};
  border-radius: 3px;
  margin: ${({ margin }) => margin};
  padding: ${({ padding }) => padding};
  color: ${({ color }) => color};
  font-size: 1em;

  ::placeholder {
    color: ${({ color }) => color};
  }
`;

export const Counter = () => (
  <div>
    <AppInput
      type="email"
      size="0.2em"
      aria-label="이메일"
      placeholder="E-Mail"
    />
    <AppInput
      type="password"
      size="0.8em"
      primary="#3bb98f"
      aria-label="비밀번호"
      placeholder="password"
    />
  </div>
);

스타일 믹스인

믹스인 패턴은 합성 방식으로 styled-components의 css() 유틸리티 함수를 사용하면 스타일 코드를 믹스인 할 수 있다.
스타일 중 중복되는 코드가 존재할 때 유용하게 사용할 수 있다.

// CSS 믹스인
const boxMixin = css`
  margin: 20px 10px;
  border: 0;
  padding: 1em;
  font-size: 15px;
  font-weight: bold;
  line-height: 1.7;
  color: #fff;
`;

// Box 컴포넌트 ⬅ CSS 믹스인
const Box = styled.div`
  ${boxMixin};
  background: #07f;
`;

// ShadowBox 컴포넌트 ⬅ CSS 믹스인
const ShadowBox = styled.div`
  ${boxMixin};
  background: #41b883;
  box-shadow: 0 6px 8px 1px rgba(0, 100, 30, 0.35);
`;

CSS prop

약간의 스타일 구성을 위해 Styled 컴포넌트를 만ㄷ르고 싶지 않은 경우 css prop을 사용하면 손쉽게 요소에 스타일 적용이 가능하다.
이 기능을 사용하려면 Babel 플러그인 설치 및 활성화가 필요하다.

import "styled-components/macro";

<Button css="padding: 0.5em 1em;" />;

Babel 플러그인은 위 코드를 아래 코드처럼 컴파일 한다.

import styled from "styled-components";

const StyledButton = styled(Button)`
  padding: 0.5em 1em;
`;

<StyledButton />;

CSS 애니메이션

@keyframes는 컴포넌트 범위 내에서 설정할 수 없다.
styled-components의 keyframes 모듈을 사용해 애니메이션을 적용할 수 있다.

import styled, { keyframes } from "styled-components";

const rotateKeyframes = keyframes`
  0%   { transform: translateY(0) }
  25%  { transform: translateY(-20px) rotate(20deg) }
  50%  { transform: translateY(10px) }
  75%  { transform: translateY(-15px) rotate(-20deg) }
  100% { transform: translateY(0) }
`;

// 마법 모자 animation 속성에 rotate 값 설정
const MagicHat = styled.div`
  font-size: 100px;
  animation: ${rotateKeyframes} 3s infinite cubic-bezier(0.35, 0.29, 0.4, 0.8);
`;

글로벌 스타일

createGlobalStyle() 함수는 글로벌 스타일을 정의하는 컴포넌트를 반환한다.
글로벌 스타일 컴포넌트 또한 속성을 전달 받아 내부에서 코드를 처리할 수 있다.

import { createGlobalStyle } from "styled-components";

const GlobalStyle = createGlobalStyle`
  body {
    margin: 0;
    font: 1rem/1.5 "Spoqa Han Sans", Sans-Serif;
    background: ${({ darken }) => (darken ? "#162442" : "#dee1e6")};
    color: ${({ darken }) => (darken ? "#dee1e6" : "#162442")};
  }
  a img {
    border: 0;
  }
`;

애플리케이션 테마

애플리케이션에 사용된 모든 컴포넌트의 스타일을 일괄적으로 변경하려면 ‘테마’를 활용한다.
컴포넌트를 작성하고 외부에서 전달한 theme prop에 따라 테마를 변경할 수 있도록 설정한다.
전달된 theme 속성이 없을 경우, props 기본 값을 attires 생성자로 설정하거나, defaultProps로 설정한다.

import styled from "styled-components";

const AppButton = styled.button.attrs(({ theme }) => ({
  theme: "main" in theme ? theme : { main: "#06f" },
}))`
  margin: 0.1em;
  border-radius: 3px;
  border: 2px solid currentColor;
  padding: 0.25em 1em;
  font-size: 1em;
  color: ${({ theme }) => theme.main};
`;

<AppButton>기본 테마 버튼</AppButton>
<AppButton theme={ main: '#25c892' }>사용자 정의 테마 버튼</AppButton>

ThemeProvider 컴포넌트를 사용해 일괄적인 테마를 제공할 수 있다.
즉, 렌더 트리 안에 존재하는 모든 스타일 컴포넌트는 테마에 접근이 가능하다.

// theme/theme.js - 테마를 관리 할 파일
export const defaultTheme = {
  color: {
    primaryGreen: "#529715",
    primaryOrange: "#e56a18",
    badgePink: "#d51754",
    badgePurple: "#910087",
    badgeGreen: "#349a2c",
    badgeNavy: "#4b5aab",
    heartPink: "#ff9d9d",
    black: "#000000",
    white: "#ffffff",

    backgroundGray: "#fafafa",
    searchGray: "#ebebeb",
    menuBg: "#252525",

    gray100: "#d8d8d8",
    gray200: "#b0b0b0",
    gray300: "#898989",
    gray400: "#616161",
    gray500: "#3a3a3a",
    gray600: "#2e2e2e",
    gray700: "#232323",
    gray800: "#171717",
    gray900: "#0c0c0c",
  },
};

// index.js
import { ThemeProvider } from "styled-components";
import { defaultTheme } from "./theme/theme";

render(
  <StrictMode>
    <ThemeProvider theme={defaultTheme}>
      <Child />
    </ThemeProvider>
  </StrictMode>,
  document.getElementById("root")
);
  • 테마 파일과 ThemeProvider 컴포넌트를 불러온 후, ThemeProvider 컴포넌트에 theme 속성을 추가한 다음 테마 모드를 설정한다.
  • ThemeProvider 컴포넌트에 전달 된 theme은 포함된 모든 컴포넌트와 공유되어 일괄적인 스타일링이 이루어진다.
  • ThemeProvider 컴포넌트는 theme 속성 전달이 꼭 필요하다.
  • ThemeProvider는 렌더링 될 때 하나 이상의 자식 컴포넌트를 반환하기 때문에 단 하나의 자식 요소만 가져야 한다.

테마 함수

theme 속성 값으로 함수를 전달할 수도 있다. 이 함수는 부모의 theme 속성 값을 전달 받는다.
예를 들어, 테마 반전같은 기능에서 사용할 수 있다.

const invertTheme = ({ fgColor, bgColor }) => ({
  fgColor: bgColor,
  bgColor: fgColor,
});

<ThemeProvider theme={theme.darkTheme}>
  <AppButton theme={invertTheme}>테마 반전</AppButton>
</ThemeProvider>;

일반 컴포넌트에 테마 적용

스타일 컴포넌트가 아닌 일반 React 컴포넌트에 테마를 사용해야 하는 경우, withTheme 고차 컴포넌트를 활용할 수 있다.

import React from "react";

// withTheme 고차 컴포넌트 불러오기
import { withTheme } from "styled-components";

class AppInput extends React.Component {
  componentDidMount() {
    // withTheme 고차 컴포넌트를 사용하면
    // 상위 영역에 설정된 테마를 전달 받습니다.
    console.log(this.props.theme);
  }
  render() {
    return <input />;
  }
}

// withTheme 고차 함수에 AppInput 컴포넌트를 전달하여 내보내기
export default withTheme(AppInput);
  • AppInput 컴포넌트는 스타일 컴포넌트는 아니지만, withTheme()로 감싸져 있어 ThemeProvider 컴포넌트의 theme 속성을 전달 받는다.
  • 전달 받은 theme을 사용하면 일반 컴포넌트에도 스타일을 적용할 수 있다.




0316

웹 서버

다채로운 웹 서버

웹 서버는 HTTP 요청을 처리하고 응답을 제공한다.
‘웹 서버’라는 용어는 웹 서버 소프트웨어와 웹페이지 제공에 특화된 장비 양쪽 모두를 가리킨다.
웹 서버의 기능은 달라도, 모든 웹 서버는 리소스에 대한 HTTP 요청을 받아서 콘텐츠를 클라이언트에게 돌려준다.

웹 서버 구현

  • 웹 서버는 HTTP 및 그와 관련된 TCP 처리를 구현한 것이다.
  • 웹 서버는 자신이 제공하는 리소스를 관리하고 웹 서버를 설정, 통제, 확장하기 위한 관리 기능을 제공한다.
  • 웹 서버는 TCP 커넥션 관리에 대한 책임을 운영체제와 나눠 갖는다.
    • 운영체제는 컴퓨터 시스템의 하드웨어를 관리하고 TCP/IP 네트워크 지원, 웹 리소스를 유지하기 위한 파일 시스템, 현재 연산 활동을 제어하기 위한 프로세스 관리를 제공한다.
  • 웹 서버는 여러 가지 형태가 가능하다.
    • 다목적 소프트웨어 웹 서버를 표준 컴퓨터 시스템에 설치하고 실행할 수 있다.
    • 어떤 회사들은 사용자에게 판매할 전자기기 안에 몇 개의 컴퓨터 칩만으로 구현된 웹 서버를 내장시켜서 완전한 관리 콘솔로 제공한다.

다목적 소프트웨어 웹 서버

  • 다목적 소프트웨어 웹 서버는 네트워크에 연결된 표준 컴퓨터 시스템에서 동작한다.
  • 아파치 같은 오픈 소스 소프트웨어를 사용할 수도 있고, 마이크로소프트의 웹 서버 같은 상용 소프트웨어를 사용할 수도 있다.
  • 웹 서버 소프트웨어는 거의 모든 컴퓨터와 운영체제에서 동작한다.

임베디드 웹 서버

  • 임베디드 웹 서버는 일반 소비자용 제품에 내장될 목적으로 만들어진 작은 웹 서버이다.
  • 임베디드 웹 서버는 사용자가 그들의 일반 소비자용 기기를 간편한 웹 브라우저 인터페이스로 관리할 수 있게 해준다.

진짜 웹 서버가 하는 일

  1. 커넥션을 맺는다 - 클라이언트의 접속을 받아들이거나, 원치 않는 클라이언트라면 닫는다.
  2. 요청을 받는다 - HTTP 요청 메시지를 네트워크로부터 읽어 들인다.
  3. 요청을 처리한다 - 요청 메시지를 해석하고 행동을 취한다.
  4. 리소스에 접근한다 - 메시지에서 지정한 리소스에 접근한다.
  5. 응답을 만든다 - 올바른 헤더를 포함한 HTTP 응답 메시지를 생성한다.
  6. 응답을 보낸다 - 응답을 클라이언트에게 돌려준다.
  7. 트랜잭션을 로그로 남긴다 - 로그 파일에 트랜잭션 완료에 대한 기록을 남긴다.

단계 1: 클라이언트 커넥션 수락

클라이언트가 이미 서버에 대해 열려있는 지속적 커넥션을 갖고 있다면, 클라이언트는 요청을 보내기 위해 그 커넥션을 사용할 수 있다.
그렇지 않다면, 클라이언트는 서버에 대한 새 커넥션을 열 필요가 있다.

새 커넥션 다루기

  • 클라이언트가 웹 서버에 TCP 커넥션을 요청하면, 웹 서버는 그 커넥션을 맺고 TCP 커넥션에서 IP 주소를 추출하여 커넥션 맞은편에 어떤 클라이언트가 있는지 확인한다.
  • 새 커넥션이 맺어지고 받아들여지면, 서버는 새 커넥션을 커넥션 목록에 추가하고 커넥션에서 오가는 데이터를 지켜보기 위한 준비를 한다.
  • 웹 서버는 어떤 커넥션이든 마음대로 거절하거나 즉시 닫을 수 있다.

클라이언트 호스트 명 식별

  • 대부분의 웹 서버는 역방향 DNS(reverse DNS)를 사용해서 클라이언트의 IP 주소를 클라이언트의 호스트 명으로 변환하도록 설정되어 있다.
  • 웹 서버는 클라이언트 호스트 명을 구체적인 접근 제어와 로깅을 위해 사용할 수 있다.
  • 호스트명 룩 업은 시간이 많이 걸릴 수 있어 웹 트랜잭션을 느려지게 할 수 있다. 많은 대용량 웹 서버는 호스트 명 분석을 꺼두거나 특정 콘텐츠에 대해서만 켜놓는다.

ident를 통해 클라이언트 사용자 알아내기

  • 몇몇 웹 서버는 IETF ident 프로토콜을 지원한다.
  • ident 프로토콜은 서버에게 어떤 사용자 이름이 HTTP 커넥션을 초기화했는지 찾아낼 수 있게 해준다.
  • 이 정보 로그는 특히 웹 서버 로깅에 유용하기 때문에, 널리 쓰이는 일반 로그 포맷의 두 번째 필드는 각 HTTP 요청을 ident 사용자 이름을 담고 있다.
  • ident는 조직 내부에서는 잘 사용할 수 있지만, 공공 인터넷에서는 여러 이유로 잘 동작하지 않는다.

단계 2: 요청 메시지 수신

커넥션에 데이터가 도착하면, 웹 서버는 네트워크 커넥션에서 그 데이터를 읽어 들이고 파싱하여 요청 메시지를 구성한다.

  • 요청줄을 파싱하여 요청 메서드, 지정된 리소스의 식별자(URI), 버전 번호를 찾는다.
  • 메시지 헤더들을 읽는다.
  • 요청 본문이 있다면, 읽어 들인다.
  • 요청 메시지를 파싱할 때, 웹 서버는 입력 데이터를 네트워크로부터 불규칙적으로 받는다. 즉, 네트워크 커넥션은 언제라도 무효화될 수 있기에 웹 서버는 파싱해서 이해하는 것이 가능한 수준의 분량을 확보할 때까지 데이터를 읽어서 메시지 일부분을 메모리에 임시로 저장해 둘 필요가 있다.

메시지의 내부 표현

  • 몇몇 웹 서버는 요청 메시지를 쉽게 다룰 수 있도록 내부의 자료 구조에 저장한다.
  • 예를 들어, 요청 메시지의 각 조각에 대한 포인터와 길이를 담을 수 있을 것이고, 헤더는 속도가 빠른 룩업 테이블에 저장되어 각 필드에 신속하게 접근할 수 있을 것이다.

커넥션 입력/출력 처리 아키텍처

  • 고성능 웹 서버는 수천 개의 커넥션을 동시에 열 수 있도록 지원한다.
  • 어떤 커넥션들로부터는 요청이 느리게 혹은 드물게 들어오고, 또 어떤 것들은 나중에 일어날 활동을 위해 조용히 대기하고 있는데 비해, 일부 커넥션들은 웹 서버로 급속히 요청을 보내고 있을 것이다.
  • 요청은 언제라도 도착할 수 있기에 웹 서버들은 항상 새 요청을 주시하고 있다.
  • 웹 서버 아키텍처의 차이에 따라 요청을 처리하는 방식도 달라진다.

단일 스레드 웹 서버

  • 단일 스레드 웹 서버는 한 번에 하나씩 요청을 처리한다.
  • 트랜잭션이 완료되면 다음 커넥션이 처리된다.
  • 구현하기에 간단하지만 처리 도중에 모든 커넥션은 무시되는 성능 문제가 존재하여 로드가 적은 서버나 진단도구에서만 적당하다.

멀티프로세스와 멀티스레드 웹 서버

  • 멀티프로세스와 멀티스레드 웹 서버는 여러 요청을 동시에 처리하기 위해 여러 개의 프로세스 혹은 고효율 스레드를 할당한다.
  • 몇몇 서버는 매 커넥션마다 스레드/프로세스 하나를 할당하지만, 서버가 수만 개의 동시 커넥션을 처리할 때 그로 인해 만들어진 수많은 프로세스나 스레드는 너무 많은 메모리나 시스템 리소스를 소비한다. 그러므로 많은 멀티스레드 웹 서비스가 스레드/프로세스의 최대 개수에 제한을 건다.

다중 I/O 서버

  • 대량의 커넥션을 지원하기 위해, 많은 웹 서버는 다중 아키텍처를 채택했다.
  • 다중 아키텍처에서는, 모든 커넥션은 동시에 그 활동을 감시당한다.
  • 커넥션의 상태가 바뀌면, 그 커넥션에 대해 작은 양의 처리가 수행된다.
  • 그 처리가 완료되면, 커넥션은 다음번 상태 변경을 위해 열린 커넥션 목록으로 돌아간다.
  • 스레드와 프로세스는 유휴 상태의 커넥션에 매여 기다리느라 리소스를 낭비하지 않는다.

다중 멀티스레드 웹 서버

  • 몇몇 시스템은 CPU 여러 개의 이점을 살리기 위해 멀티 스레등과 다중화를 결합한다.
  • 여러 개의 스레드는 각각 열려있는 커넥션을 감지하고 각 커넥션에 대해 조금씩 작업을 수행한다.

단계 3: 요청 처리

웹 서버가 요청을 받으면, 서버는 요청으로부터 메서드, 리소스, 헤더, 본문을 얻어내어 처리한다.

단계 4: 리소스의 매핑과 접근

  • 웹 서버는 리소스 서버다. HTML이나 JPEG 이미지 같은 미리 만들어진 콘텐츠를 제공하며, 서버 위에서 동작하는 리소스 생성 애플리케이션을 통해 만들어진 동적 콘텐츠도 제공한다.
  • 웹 서버가 클라이언트에 콘텐츠를 전달하려면, 그전에 요청 메시지의 URI에 대응하는 알맞은 콘텐츠나 콘텐츠 생성기를 웹 서버에서 찾아서 그 콘텐츠의 원천을 식별해야 한다.

Docroot

  • 웹 서버는 여러 종류의 리소스 매핑을 지원하는데, 그 중 가장 단순한 형태는 요청 URI를 웹 서버 파일 시스템 안에 있는 파일 이름으로 사용하는 것이다.
  • 일반적으로 웹 서버 파일 시스템의 특별한 폴더를 웹 콘텐츠를 위해 예약해 둔다. 이 폴더는 문서 루트 혹은 docroot로 불린다.
  • 웹 서버는 요청 메시지에서 URI를 가져와서 문서 루트 뒤에 붙인다.

가상 호스팅된 docroot

  • 가상 호스팅 웹 서버는, 각 사이트에 그들만의 분리된 문서 루트를 주는 방법으로 한 웹 서버에서 여러 개의 웹 사이트를 호스팅한다.
  • 가상 호스팅 웹 서버는 URI나 Host 헤더에서 얻은 IP 주소나 호스트 명을 이용해 올바른 문서 루트를 식별한다.
  • 이 방법으로, 하나의 웹 서버 위에서 두 개의 사이트가 완전히 분리된 콘텐츠를 갖고 호스팅 되도록 할 수 있다.

사용자 홈 디렉터리 docroot

  • docroot의 또 다른 대표적인 활용은, 사용자들이 한 대의 웹 서버에서 각자의 개인 웹 사이트를 만들 수 있도록 해주는 것이다.
  • 보통 빗금과 물결표 다음에 사용자 이름이 오는 것으로 시작하는 URI는 그 사용자의 개인 문서 루트를 가리킨다.

디렉터리 목록

  • 웹 서버는 경로가 파일이 아닌 디렉터리를 가리키는, 디렉터리 URL에 대한 요청을 받을 수 있다.
  • 대부분의 웹 서버는 클라이언트가 디렉터리 URL을 요청했을 때 다음과 같이 몇 가지 다른 행동을 취하도록 설정할 수 있다.
  • 대부분의 웹 서버는 요청한 URL에 대응되는 디렉터리 안에서 index.html으로 이름 붙은 파일을 찾는다.

동적 콘텐츠 리소스 매핑

  • 웹 서버는 URI를 동적 리소스에 매핑할 수도 있다. 즉, 요청에 맞게 콘텐츠를 생성하는 프로그램에 URI를 매핑하는 것이다.
  • 사실 웹 서버들 중에서 애플리케이션 서버라고 불리는 것들은 웹 서버를 복잡한 백엔드 애플리케이션과 연결하는 일을 한다.
  • 어떤 리소스가 동적 리소스라면, 애플리케이션 서버는 그에 대한 동적 콘텐츠 생성 프로그램이 어디에 있는지, 그리고 어떻게 그 프로그램을 실행하는지 알려줄 수 있어야 한다.

서버사이드 인클루드 (SSI)

  • 많은 웹 서버가 서버사이드 인클루드도 지원한다. 만약 어떤 리소스가 서버사이드 인클루드를 포함하고 있는 것으로 설정되어 있다면, 서버는 그 리소스의 콘텐츠를 클라이언트에게 보내기 전에 처리한다.
  • 서버는 콘텐츠에 변수 이름이나 내장된 스크립트가 될 수 있는 어떤 특별한 패턴이 있는지 검사를 받는다. 특별한 패턴은 변수 값이나 실행 가능한 스크립트의 출력 값으로 치환된다.

접근 제어

  • 웹 서버는 또한 각각의 리소스에 접근 제어를 할당할 수 있다.
  • 접근 제어되는 리소스에 대한 요청이 도착했을 때 웹 서버는 클라이언트의 IP 주소에 근거하여 접근을 제어할 수 있고 혹은 리소스에 접근하기 위한 비밀번호를 물어볼 수도 있다.

단계 5: 응답 만들기

서버가 리소스를 식별하면, 서버는 요청 메서드로 서술되는 동작을 수행한 뒤 응답 메시지를 반환한다.

응답 엔터티

트랜잭션이 응답 본문을 생성한다면, 그 내용을 응답 메시지와 함께 돌려보낸다.
만약 본문이 있다면, 응답 메시지는 주로 다음을 포함한다.

  • 응답 본문의 MIME 타입을 서술하는 Content-Type 헤더
  • 응답 본문의 길이를 서술하는 Content-Length 헤더
  • 실제 응답 본문의 내용

MIME 타입 결정하기

웹 서버에게는 응답 본문의 MIME 타입을 결정해야 하는 책임이 있다.

mime.types

  • 웹 서버는 MIME 타입을 나타내기 위해 파일 이름의 확장자를 사용할 수 있다.
  • 웹 서버는 각 리소스의 MIME 타입을 계산하기 위해 확장자별 MIME 타입이 담겨있는 파일을 탐색한다. 이러한 확장자 기반 타입 연계가 가장 흔한 방법이다.

매직 타이핑

  • 아파치 웹 서버는 각 파일의 MIME 타입을 알아내기 위해 파일의 내용을 검사해서 알려진 패턴에 대한 테이블(매직 파일이라 불림)에 해당하는 패턴이 있는지 찾아볼 수 있다.
  • 이 방법이 느리긴 하지만 파일이 표준 확장자 없이 이름 지어진 경우에는 특히 편리하다.

유형 명시

  • 특정 파일이나 디렉터리 안의 파일들이 파일 확장자나 내용에 상관없이 어떤 MIME 타입을 갖도록 웹 서버를 설정할 수 있다.

유형 협상

  • 어떤 웹 서버는 한 리소스가 여러 종류의 문서 형식에 속하도록 설정할 수 있다.
  • 이 때 웹 서버가 사용자와의 협상 과정을 통해 사용하기 가장 좋은 형식을 판별할 것인지의 여부도 설정할 수 있다.

리다이렉션

웹 서버는 요청을 수행하기 위해 브라우저가 다른 곳으로 가도록 리다이렉트 할 수 있다.

영구히 리소스가 옮겨진 경우

  • 웹 서버는 클라이언트에게 리소스의 이름이 바뀌었으므로, 클라이언트는 북마크를 갱신하거나 할 수 있다고 말해줄 수 있다.
  • 301 상태 코드는 이런 종류의 리다이렉트를 위해 사용된다.

임시로 리소스가 옮겨진 경우

  • 서버는 클라이언트가 처음에는 리다이렉트하길 원하고, 나중에는 원래 URL로 찾아오고 북마크도 갱신하지 않기를 원한다.
  • 303과 307 상태 코드는 이런 종류의 리다이렉트를 위해 사용된다.

URL 증강

  • 서버는 문맥 정보를 포함시키기 위해 재 작성된 URL로 리다이렉트한다.
  • 요청이 도착했을 때, 서버는 상태 정보를 내포한 새 URL을 생성하고 사용자를 이 새 URL로 리다이렉트한다. 이것은 트랜잭션 간 상태를 유지하는 유용한 방법이다.
  • 303과 307 상태 코드는 이런 종류의 리다이렉트를 위해 사용된다.

부하 균형

  • 만약 과부하된 서버가 요청을 받으면, 서버는 클라이언트를 좀 덜 부하가 걸린 서버로 리다이렉트할 수 있다.
  • 303과 307 상태 코드는 이런 종류의 리다이렉트를 위해 사용된다.

친밀한 다른 서버가 있을 때

  • 웹 서버는 어떤 사용자에 대한 정보를 가질 수 있다.
  • 서버는 클라이언트를 그 클라이언트에 대한 정보를 갖고 있는 다른 서버로 리다이렉트할 수 있다.
  • 303과 307 상태 코드는 이런 종류의 리다이렉트를 위해 사용된다.

디렉터리 이름 정규화

  • 클라이언트가 디렉터리 이름에 대한 URI를 요청하는데 끝에 빗금을 빠뜨렸다면, 대부분의 웹 서버는 상대 경로가 정상적으로 동작할 수 있도록 클라이언트를 슬래시를 추가한 URI로 리다이렉트한다.

단계 6: 응답 보내기

  • 요청 받을 때와 마찬가지로 비슷한 이슈에 직면한다. 많은 커넥션 중 일부는 아무것도 안하고 있는 상태이고, 일부는 서버로 데이터를 보내고 있으며, 또 일부는 클라이언트로 돌려줄 응답 데이터를 실어 나르고 있을 것이다.
  • 서버는 커넥션 상태를 추적해야 하며 지속적인 커넥션은 특별히 주의해서 다룰 필요가 있다. 비지속적인 커넥션이라면, 서버는 모든 메시지를 전송했을 때 자신쪽의 커넥션을 닫을 것이다.
  • 지속적인 커넥션이라면, 서버가 Content-Length 헤더를 바르게 계산하기 위해 특별한 주의를 필요로 하는 경우나, 클라이언트가 응답이 언제 끝나는지 알 수 없는 경우에, 커넥션은 열린 상태를 유지할 것이다.

단계 7: 로깅

마지막으로, 트랜잭션이 완료되었을 때 웹 서버는 트랜잭션이 어떻게 수행되었는지에 대한 로그를 로그파일에 기록한다.
대부분의 웹 서버는 로깅에 대한 여러 가지 설정 양식을 제공한다.




0317

TCP (흐름제어/혼잡제어/오류제어)

흐름제어

송신측과 수신측 사이의 데이터 처리 속도 차이를 제어하기 위한 기법으로 데이터 처리 속도를 조절하여 수신자의 버퍼 오버플로우를 방지한다.

  • 송신하는 곳에서 감당 안되게 많은 데이터를 빠르게 보내 수신하는 곳에서 문제가 일어나는 것을 막는다.
  • 만약, 송신측의 전송량 > 수신측의 수신량일 경우 전송된 패킷은 수신측의 큐를 넘어서 손실될 수 있기 때문에 송신측의 패킷 전송량을 제어하게 된다.

Stop and Wait

  • 매번 전송한 패킷에 대한 확인 응답을 받아야 다음 패킷을 전송할 수 있다. 이러한 구조로 인해 비효율적이라는 단점이 있다.

슬라이딩 윈도우

  • 윈도우 : 송신, 수신 스테이션 양쪽에서 만들어진 버퍼의 크기
  • 수신측에서 설정한 윈도우 크기만큼 송신측에서 확인 응답 없이 세그먼트를 전송할 수 있게 하여 데이터 흐름을 동적으로 조절하는 기법이다. 따라서 송신측에서는 ACK 프레임을 수신하지 않더라도 여러 개의 프레임을 연속적으로 전송할 수 있다.
  • 송신측에서 0, 1, 2, 3, 4, 5, 6을 보낼 수 있는 프레임을 가지고 있고 데이터 0, 1을 전송했다고 가정하면 슬라이딩 윈도우 구조는 2, 3, 4, 5, 6처럼 변하게 된다. 이때 만약 수신측으로부터 ACK라는 프레임을 받게 된다면 송신 측은 수신측에서 정상적으로 받았음을 알게 되고 송신측의 슬라이딩 윈도우는 ACK 프레임의 수만큼 오른쪽으로 경계가 확장된다.
  • Stop and Wait의 비효율성을 개선했다.

오류 제어

오류 검출과 재전송을 포함한다.
ARQ(Automatic Repeat Request) 기법을 사용해 프레임이 손상되었거나 손실되었을 경우, 재전송을 통해 오류를 복구한다. ARQ 기법은 흐름 제어 기법과 연관되어 있다.

Stop and Wait ARQ

  • 송신 측에서 1개의 프레임을 송신하고, 수신측에서 수신된 프레임의 에러 유무에 따라 ACK 혹은 NAK를 보내는 방식이다.
  • 식별을 위해 데이터 프레임과 ACK 프레임은 각각 0, 1 번호를 번갈아가며 부여한다.
  • 수신측에 데이터를 받지 못했을 경우 NAK를 보내고, NAK를 받은 송신측은 데이터를 재전송한다.
  • 만약, 데이터나 ACK가 분실되었을 경우 일정 간격의 시간을 두고 타임아웃이 되면 송신측은 데이터를 재전송한다.

Go-Back-n ARQ

  • 전송된 프레임이 손상되거나 분실된 경우 그리고 ACK 패킷의 손실로 인한 TIME_OUT이 발생한 경우, 확인된 마지막 프레임 이후로 모든 프레임을 재전송한다.
  • 슬라이딩 윈도우는 연속적인 프레임 전송 기법으로 전송측은 전송된 프레임의 복사본을 가지고 있어야하며, ACK와 NAK 모두 각각 구별해야 한다.
  • ACK: 다음 프레임을 전송
  • NAK: 손상된 프레임 자체 번호를 반환

재전송 되는 경우

  1. NAK 프레임을 받았을 경우
  2. 전송 데이터 프레임의 분실
    • 수신측에서 데이터 1을 받고 다음 데이터로 3을 받게 된다면 데이터 2를 받지 못했으므로 수신측에서는 데이터 3을 폐기하고 데이터 2를 받지 못했다는 NAK2를 전송측에 보낸다.
    • 수신측은 기존에 받았던 데이터 중 NAK(n)으로 보냈던 대상 데이터 이후의 모든 데이터를 폐기하고 재전송 받는다.
  3. 지정된 타임 아웃 내의 ACK 프레임 분실
    • 전송측은 분실된 ACK를 다루기 위해 타이머를 가지고 있다. 또한 전송측에서는 이 타이머의 타임 아웃 동안 수신측으로부터 ACK 데이터를 받지 못했을 경우, 마지막 ACK된 데이터부터 재전송한다.

SR(Selective-Reject) ARQ

  • GBn ARQ의 확인된 마지막 프레임 이후의 모든 프레임을 재전송하는 단점을 보완하는 기법이다.
  • SR ARQ는 손상되거나 손실된 프레임만 재전송한다.
  • 그렇기 때문에 별도의 데이터 재정렬을 수행해야 하며, 별도의 버퍼를 필요로 한다.
  • 수신측에 버퍼를 두어 받은 데이터의 정렬이 필요하다.

혼잡제어

  • 송신측의 데이터 전달과 네트워크 데이터 처리 속도를 해결하기 위한 기법
  • 한 라우터에게 데이터가 몰려 모든 데이터를 처리할 수 없는 경우, 호스트들은 재전송을 하게 되고 결국 혼잡을 가중시켜 오버플로우나 데이터 손실이 발생한다.
  • 이러한 네트워크 혼잡을 피하기 위해 송신측에서 보내는 데이터의 전송 속도를 제어하는 것이 혼잡 제어의 개념이다.

AIMD(Additive Increase Multicative Decrease)

  • 함 증가/곱 감소 알고리즘이라고 한다.
  • 처음에 패킷 하나를 보내는 것으로 시작하여 전송한 패킷이 문제 없이 도착한다면 Window Size를 1씩 증가시키며 전송하는 방법이다. 만약, 패킷 전송을 실패하거나 TIME_OUT이 발생하면 Window Size를 절반으로 감소시킨다.
  • 이 방식을 사용하는 여러 호스트가 한 네트워크를 공유하고 있으면 나중에 진입하는 쪽이 처음에는 불리하지만 시간이 흐르면 평형 상태로 수렴하게 되는 특징이 있다.
  • 문제점은 초기에 네트워크의 높은 대역폭을 사용하지 못해 오랜 시간이 걸리게 되고, 네트워크가 혼잡해지는 상황을 미리 감지하지 못한다. 즉, 네트워크가 혼잡해지고 나서야 대역폭을 줄이는 방식이다.

Slow Start

  • AIMD가 네트워크 수용량 주변에서는 효율적으로 동작하지만, 처음에 전송 속도를 올리는데 시간이 너무 길다는 단점이 있다.
  • Slow Start는 AIMD와 마찬가지로 패킷을 하나씩 보내는 것부터 시작한다. 이 방식은 ACK 패킷마다 Window Size를 1씩 늘린다. 즉, 한 주기가 지나면 Window Size는 2배가 된다.
  • 혼잡 현상이 발생하면 Window Size를 1로 떨어뜨린다.
  • 처음에는 네트워크의 수용량을 예측할 수 있는 정보가 없지만 한 번 혼잡현상이 발생하고 나면 네트워크 수용량을 어느정도 예상할 수 있으므로 혼잡 현상이 발생하였던 Window Size 절반까지는 지수 함수 꼴로 증가시키고, 그 이후부터는 원만하게 1씩 증가시키는 방식이다.
  • 미리 정해진 임계값에 도달할 때까지 윈도우 크기를 1씩 증가시키고, 전송되는 데이터의 크기가 임계 값에 도달하면 혼잡 회피 단계로 넘어간다.

혼잡 회피

  • 윈도우의 크기가 임계 값에 도달한 이후에는 데이터 손실 발생 확률이 높다.
  • 이를 회피하기 위해 윈도우 크기를 선형적으로 1씩 증가시키는 방법이다.
  • 수신측으로 일정 기간동안 ACK를 수신하지 못한 경우
    • 타임 아웃 발생하면 네트워크 혼잡이 발생했다고 인식하고, 윈도우의 크기 세그먼트의 수를 1로 감소시킨다.
    • 그리고 임계값을 패킷 손실이 발생했을 때의 윈도우 크기의 절반으로 줄인다.

빠른 회복

  • 혼잡 상태가 되면 Window Size를 1로 줄이지 않고 절반으로 줄이고 선형 증가시키는 방법
  • 빠른 회복 정책까지 적용하면 혼잡 상황을 한 번 겪고 나서부터는 순수한 AIMD 방식으로 동작

빠른 재전송

  • 수신측에서 패킷을 받을 때 먼저 도착해야 할 패킷이 도착하지 않고 다음 패킷이 도착한 경우에도 ACK 패킷을 보낸다. 단, 순서대로 잘 도착한 마지막 패킷의 다음 패킷의 순번을 ACK 패킷에 실어서 보낸다.
  • 따라서 중간에 패킷 하나가 손실되면 송신측에서는 순번이 중복된 ACK 패킷을 받게 된다. 이것을 감지하면 문제가 되는 순번의 패킷을 재전송할 수 있다.
  • 빠른 재전송은 중복된 순번의 패킷을 3개 받으면 재전송한다. 그리고 혼잡이 발생한 것으로 간주하여 Window Size를 절반으로 줄인다.




0320

NextJS 기본 개념

설치

npx create-next-app@latest

framework vs library

라이브러리는 개발자가 필요할 때 불러와서 사용하는 것이다.
예를 들어, 리액트 앱을 만든다고 가정하면 어느 정도 자유도를 보장 받으며 내가 원하는 방식대로 폴더를 구조화하고, 파일을 위치시켜 프로젝트를 진행할 수 있다.
반면 프레임워크는 개발자의 코드를 불러오는 것이다.
next.js 앱을 만들 때는 next.js가 지정한 방식대로 코드를 위치시켜 사용해야 한다. 이는 자유도는 떨어지지만 귀찮은 반복 작업들을 프레임워크가 대신 해주어 편하게 개발할 수 있다.

pages

  • 이 폴더에 파일을 위치시키고 컴포넌트를 반환하면, 파일명을 url로 라우팅하여 컴포넌트를 화면에 노출시킨다.
  • 컴포넌트는 export default로 내보내야 하고, 컴포넌트의 이름은 중요하지 않다.
  • 단, index.js는 홈과 같은 역할을 하는 파일로 슬래쉬/로 접근할 수 있다.
  • JSX를 사용할 때 import react from 'react';와 같은 코드가 필요하지 않다.
  • 단, useState와 같은 훅을 사용할 때는 import가 필요하다.

Static Pre Rendering

  • next.js의 좋은 기능 중 하나는, 앱에 있는 페이지들이 미리 렌더링되는 것이다.
  • cra로 구성한 앱은 처음에 div 태그만 가지고 있고, 그 이후 자바스크립트를 통해 렌더링을 수행한다.
    • 그렇기에 느린 네트워크 환경을 가진 사용자들에게는 첫 화면을 로딩하는 데 오랜 시간이 걸린다.
    • 그리고 자바스크립트가 활성화 되어 있지 않은 상황에서는 <noscript> 안에 있는 텍스트가 렌더링 된다.
  • 반면 next.js로 구성된 앱은 유저가 매우 느린 연결을 하고 있거나, 자바스크립트가 완전히 비활성화 되어 있어도 유저는 HTML을 볼 수 있다.
    • next.js는 react.js를 백엔드에서 동작시켜서 페이지를 미리 만들고, 컴포넌트를 render 시킨다.
    • 그리고 react.js를 실행시켜 상호작용 가능한 페이지로 만드는데, 이처럼 react.js를 프론트엔드 안에서 실행하는 것을 hydration이라고 한다.

Routing

  • next.js에서 새로고침을 하지 않고 클라이언트 사이드 네비게이션을 사용하기 위해서는 <Link> 태그를 사용한다.
  • <Link> 태그 속성으로 href를 주고, 자식 요소로 href를 가지지 않은 anchor 태그를 가진다.
  • anchor 태그에 className이나 style같은 추가 어트리뷰트를 주기 위해서는 anchor 태그에 주어야 한다. Link 태그에 주면 적용되지 않는다.
  • next.js에서 제공하는 location에 대한 정보를 얻기 위한 훅이 있는데, 그것은 useRouter()이다.

CSS Modules

  • next.js에서 스타일을 관리하는 방법 중 하나는 CSS 모듈을 사용하는 것이다.
  • 파일명.module.css 확장자를 가지는 파일로 중요한 것은 확장자명이다.
  • 그리고 이 파일을 import styles from "./NavBar.module.css"와 같이 자바스크립트 오브젝트로서 임포트시킨다.
  • 주의할 점은 .nav라는 클래스를 적용시킬 때, className="nav"가 아닌 className={styles.nav}처럼 사용해야 한다. 즉, 자바스크립트 오브젝트 프로퍼티에 접근하듯 사용한다.
  • 이러한 접근 방식의 장점으로는 클래스명에 무작위 텍스트를 적용하여 클래스명 충돌을 일으키지 않는다.
  • 하나의 태그에 어떤 클래스는 삽입하고, 어떤 클래스는 조건에 따라 삽입되어야 한다면 백틱을 이용한 문자열 삽입 방식이나 배열로 각 클래스를 표기하고, join 메서드로 문자열을 만들어 적용할 수 있다.
  • CSS 모듈 방식의 단점은 별도의 CSS 파일을 가져야 한다는 것과 클래스 이름 자체를 구현하는 것이 복잡하다는 점이다.

Styles JSX

  • Next.js에서 스타일을 추가하는 또 다른 방법으로 Next.js의 고유의 방법이다.
  • 사용 방법은 반환하는 컴포넌트에 html style 태그를 사용하여 jsx라는 prop을 준 뒤, 사용할 css를 정의한다.
export default function NavBar() {
  return (
    <nav>
      <Link href="/">
        <a>Home</a>
      </Link>
      <Link href="/about">
        <a>About</a>
      </Link>
      <style jsx>{`
        nav {
          background-color: tomato;
        }
        a {
          text-decoration: none;
        }
      `}</style>
    </nav>
  );
}
  • 이는 정상적으로 동작하고, 클래스 이름은 CSS 모듈 방식보다도 더 기이한 방식으로 작명된다.
  • 이 방법 역시 모듈들이 독립되어 존재한다.
  • 이 방식은 파일 import도 필요 없고, 많은 경우 클래스 이름에 대해 생각할 필요 없이 태그명을 사용할 수 있다.
  • 그리고 부모 컴포넌트가 동일한 클래스명을 사용해도 상관이 없다.

Custom App

  • 글로벌 스타일을 적용하기 위한 가장 쉬운 방법은 <style jsx global> prop을 추가하는 것이다.
  • 하지만 이 방식은 페이지를 고려하지 않은 방식으로 이를 적용하기 위해서는 모든 페이지에 이 스타일을 적용해야 하는데 이는 비효율적이다.
  • 모든 페이지를 설정하기 위해서는 pages 폴더에 _app.js라는 파일을 생성해야 한다.
    • 이는 모든 페이지의 청사진을 커스텀 할 수 있는 장소이다.
    • 이는 기본으로 프레임워크 내에 포함되는 것으로 무조건 이 이름으로 생성해야만 한다.
    • Next.js는 모든 페이지를 렌더링하기 전에, 이 페이지를 확인한다.
export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}
  • 이는 개발자가 _app.js 파일을 건드리지 않으면 동작하는 기본 코드이다.
  • 이에 맞춰서 적절한 코드를 입력하면 모든 페이지를 렌더링할 때 이 페이지를 참고하여 렌더링한다.
  • Next.js로 앱을 만들때는 global css파일을 임포트 할 수 없지만, 커스텀 App 파일에서는 가능하다.

태그:

카테고리:

업데이트: