컴포넌트 내부의 useCallback 남용 이대로 괜찮은걸까?

진행 중인 프로젝트에서 React 함수 컴포넌트 내부에서 사용하는 함수들의 경우, 재사용(자식에게 내려주는 Props 용도 등)에 대한 안정성이라는 명목으로 useCallback으로 대부분 감싸주고 있었다.

memoization이라는 기능 자체 그리고 종속성의 존재가 어떠한 영향을 주는지 모른 채, 이대로 사용하는 것이 옳은 것인가에 대한 의문이 들었다.

Memoization 기능은 공짜가 아니다!

(참고자료)React Memoization(useMemo vs. useCallback vs. React.memo) 알아보기
React에서 활용할 수 있는 Memoization 기능들에 대한 학습을 토대로 Memoization 기능을 활용한 성급한 최적화 시도가 오히려 성능을 저하시킬 수 있다는 것을 확인할 수 있었다.

React의 useCallback Hook 경우, 함수를 재정의해야 하는지 여부를 결정하기 위해 다시 렌더링할 때마다 종속성 배열의 종속성을 비교해야 하고 이 계산은 오히려 단순히 함수를 재정의하는 것보다 비용이 클 수 있다.

이에, 이러한 Memoization 기능에 대해 능동적으로 접근하는 것 보다는 성능 문제를 도출한 후에 이를 개선하기 위한 대응책으로 활용하는 등, 분명히 필요할 때 근거를 가지고 사용하는 것이 옳다는 결론을 내릴 수 있었다.

최소한 기존에 사용하던대로 함수 컴포넌트 내부의 함수들을 모두 useCallback으로 래핑하는 것만큼은 하지 말아야겠다고 생각했다.

Refactoring

(예) 회원가입화면 ID 입력 컴포넌트

Before

/** @jsxImportSource @emotion/react */

import React, { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import { css } from '@emotion/react';

import { API, RESULT } from 'utils/constants';

import {
  // ...중략...
} from './styles';

export const IdInput = ({ setId }: { setId: Dispatch<SetStateAction<string>> }) => {
  const [idDraft, setIdDraft] = useState<string>('');
  const [idWarning, setIdWarning] = useState<string>('');
  const [idDuplicationCheckResult, setIdDuplicationCheckResult] = useState<string>('');

  // 아이디값 입력에 따른 상태관리
  const handleOnChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setIdDraft(e.target.value);
  }, []);

  // 서버측 id 유효성 검사를 위해 fetch 통신(쿼리스트링)
  const sendIdToServer = useCallback(() => {
    fetch(`${process.env.REACT_APP_FETCH_URL}${API.VALIDATE}?${new URLSearchParams({ id: idDraft })}`)
      .then((res) => res.json())
      // ...중략...
  }, [idDraft]);

  // 클라이언트측 id 유효성 검사
  // 아이디 요소 확인
  const isValidIdStr = useCallback((id: string) => {
    // ...중략...
  }, []);

  // 아이디 길이 확인
  const isValidIdLength = useCallback((id: string) => {
    // ...중략...
  }, []);

  // 아이디 유효성 검사
  const isValidId = useCallback(() => {
    // ...중략...
  }, [idDraft]);

  // id값이 유효하면 서버로 보내주기
  const handleClick = useCallback(() => {
    // ...중략...
  }, [idDraft]);

  // 사용자가 id값을 입력할때마다 검사
  useEffect(() => {
    // ...중략...
  }, [idDraft]);

  const isAllValid = useCallback(() => {
    // ...중략...
  }, [idWarning, idDuplicationCheckResult]);

  return (
    <div>
      <div css={registerPageInputWrapperStyle}>
        <input
          css={css(registerPageInputStyle, { width: 300 })}
          placeholder='아이디'
          value={idDraft}
          onChange={handleOnChange}
        />
        <button type='button' css={registerPageIdButtonStyle} onClick={handleClick}>
          <span>중복확인</span>
        </button>
      </div>
      {isAllValid() !== RESULT.NULL && (
        <span css={idValidationStyle(isAllValid())}>
          {isAllValid() === RESULT.FAIL ? idWarning : idDuplicationCheckResult}
        </span>
      )}
    </div>
  );
};

Refactoring 과정

useCallback 남용 리팩토링

  • useCallback을 모두 지운 후, useCallback이 필요하거나 useCallback을 적용해도 비용이 크지 않으리라 생각되는 함수에만 적용하고자 하였음
// 사용자의 입력값 변화마다 호출되므로 useCallback으로 최적화
  const handleOnChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setIdDraft(e.target.value);
  }, []);

// id값이 유효하면 서버로 보내주기
  // 버튼 클릭이 발생할 때만 일어나는 이벤트이고 id입력 시마다 client측 유효성 검사를 진행하고 있으므로 굳이 useCallback을 적용할만큼 자주 일어나진 않음
  const handleClick = () => {
    if (!isValidId(idDraft)) {
      return;
    }
    // 아이디값 서버측 유효성 검사
    checkIdServerValidation(idDraft)

// ...중략...
  • 기존에 useCallback으로 감싸져있던 내부 함수들 중에 자주 사용되지만 함수 컴포넌트 내의 상태관리에 관여하지 않고 주요 비즈니스 로직이라 판단되지 않는 함수는 별도 util 파일에 분리 후 import해서 사용
// 기존 id 유효성 검사를 별도 util파일로 분리
import { isValidId, isValidIdLength, isValidIdStr } from './util';

기타 추가 Refactoring 사항

  • 비즈니스 로직을 파악하기 쉽도록 fetch기능은 service.ts라는 별도 service 파일로 분리
import { checkIdServerValidation } from './service';
  • id 유효성에 대해 여러개로 나뉘어져 있던 상태 기능을 하나의 상태로만 관리할 수 있도록 통합하고 상태코드와 상태안내문은 상수화하여, 코드 안정성을 높임
import { VALIDATION_INFO, VALIDATION_RESULT } from './constants';

const [validationType, setValidationType] = useState<number>(VALIDATION_RESULT.NULL);
// constants.ts

export const VALIDATION_RESULT: Readonly<Record<string, number>> = {
  NULL: 0,
  SUCCESS: 1,
  WRONG_STR: 2,
  WRONG_LENGTH: 3,
  DUPLICATED: 4,
  CLIENT_FAIL: 5,
};

export const VALIDATION_INFO: Readonly<Record<string, string>> = {
  1: '유효한 아이디 입니다.',
  2: '알파벳과 숫자로만 이루어져야 합니다.',
  3: '4글자 이상 15글자 이하만 가능합니다.',
  4: '중복되는 Id 입니다.',
  5: '4글자 이상, 15글자 이하의 알파벳과 숫자로 작성바랍니다.',
};

After

/** @jsxImportSource @emotion/react */

import React, { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';

import { checkIdServerValidation } from './service';
import { isValidId, isValidIdLength, isValidIdStr } from './util';
import { VALIDATION_INFO, VALIDATION_RESULT } from './constants';

import { idButtonStyle, idInputStyle, idInputWrapperStyle, idValidationStyle } from './idInput.styles';

export const IdInput = ({ setId }: { setId: Dispatch<SetStateAction<string>> }) => {
  // 유효성이 확정되지 않은 예비 ID 값
  const [idDraft, setIdDraft] = useState<string>('');
  const [validationType, setValidationType] = useState<number>(VALIDATION_RESULT.NULL);

  // 아이디값 입력에 따른 상태관리
  // 사용자의 입력값 변화마다 호출되므로 useCallback으로 최적화
  const handleOnChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setIdDraft(e.target.value);
  }, []);

  // id값이 유효하면 서버로 보내주기
  // 버튼 클릭이 발생할 때만 일어나는 이벤트이고 id입력 시마다 client측 유효성 검사를 진행하고 있으므로 굳이 useCallback을 적용할만큼 자주 일어나진 않음
  const handleClick = () => {
      // ...중략...
  };

  // 사용자가 id값을 입력할때마다 유효성 검사 결과를 알려주어 UX 향상
  useEffect(() => {
    // ...중략...
  }, [idDraft]);

  return (
    <>
      <div css={idInputWrapperStyle(validationType)}>
        <input placeholder='아이디' value={idDraft} onChange={handleOnChange} css={idInputStyle} />
        <button type='button' onClick={handleClick} css={idButtonStyle}>
          <span>중복확인</span>
        </button>
      </div>
      {validationType !== VALIDATION_RESULT.NULL && (
        <span css={idValidationStyle(validationType)}>{VALIDATION_INFO[validationType]}</span>
      )}
    </>
  );
};

이번 리팩토링은 성능에 유의미한 영향이 있었을까?

테스팅 동작

(참고) 테스트을 위한 웹페이지 동작 영상

Before

  1. 라이트하우스 결과 
  2. (주요 리팩토링 대상) IdInput 컴포넌트 검사 결과
  • 성능 
  • Profiler 

After

  1. 라이트하우스 결과 
  2. (주요 리팩토링 대상) IdInput 컴포넌트 검사 결과
  • 성능 
  • Profiler 

결과에 대한 분석

  • 비교결과, 라이트하우스 결과에서 보듯이 useCallback을 사용하지 않더라도 전반적인 성능에 큰 영향이 없었다.
  • 오히려, 리팩토링 과정에서 추가적으로 내부에서 useState로 관리하는 상태값을 줄인 덕분에 오히려 랜더링 횟수가 감소하였고 속도도 빨라졌다.
  • 이를 통해, useCallback의 유무가 현재 코드 상에서는 전체적인 성능에 영향을 주지 않았고 리팩토링 과정에서 추가적으로 진행한 상태값을 줄이는 리팩토링이 성능향상에 유의미한 결과를 주었음을 확인할 수 있었다.
  • useCallback의 사용 유무와 상관없다면, 코드복잡성을 줄이기 위해 useCallback을 사용하지 않는 것이 좋을 것이다.

앞으로 남은 과제

  • 앞으로는 다른 페이지(컴포넌트)들에 대해서도 성능분석을 진행한 후에, 분석 결과를 바탕으로 React Memoization(useMemo, useCallback, React.memo 등) 기능이 필요한 지에 대한 근거를 먼저 검토하고 필요한 경우에 적재적소에 활용하여 성능을 향상시킬 수 있도록 하고자 한다.

부탁드리는 사항

혹시 잘못된 내용이나, 문제의 소지가 되는 내용이 있다면 언제든 알려주시면 큰 도움이 될 것 같습니다! 또한, 미처 고려하지 못한 Refactoring 사항에 대해서도 피드백 주시면 언제든 환영입니다!

코딩테스트 연습 - 할인 행사

문제요약

일정금액 지불 → 10일간 회원

회원 대상 매일 1가지 제품 할인

할인 제품은 하루에 하나씩만

정현이는 자신이 원하는 제품과 수량이 할인하는 날짜와 10일 연속으로 일치한 경우 회원가입 하려고 함

정현이가 가입 가능한 날짜의 총 일수 return

없으면 return 0

입력

1 ≤ want(원하는 제품 문자열 배열), number(원하는 수량 배열) ≤ 10

10 ≤ discount 배열 길이 ≤ 10^5

1 ≤ number의 원소 ≤ 10

number의 원소 합 = 10

출력

return int

불가능하면, return 0

어떻게 풀까?

discount 범위를 생각할 때, O(NlogN) 정도까지 가능

회원가입 일수가 10일 → 슬라이딩 윈도우?

⇒ 회원가입 일수가 10일일 뿐, 1일 안에도 정현이의 want가 모두 채워질 수 있음

투포인터로 적용해서 discount for문 한 바퀴(10^5) 돌리면서 조정해주고 그 안에서 want 길이(10)만큼 돌면서 모두 충족되었는지 검사해도 시간복잡도 충분할듯

내 코드

def check_satisfied(want_num):
    for want in want_num.keys():
        if want_num[want] > 0:
            return False
    return True

def solution(want, number, discount):
    # 데이터 전처리(want, number-> dict)
    want_num = dict()
    for i in range(len(want)):
        want_num[want[i]] = number[i]
    # answer 초기화
    answer = 0
    # discount 돌면서 투포인터 적용
    start = 0
    end = 0
    # 초기화 처리
    if discount[start] in want and want_num[discount[start]] > 0:
        want_num[discount[start]] -= 1
    while end < len(discount):
        days = end - start + 1
        # 10일 초과 -> start += 1
        if days > 10:
            if discount[start] in want:
                want_num[discount[start]] += 1
            start += 1
        # 10일 전까지 -> 검사
        elif check_satisfied(want_num):
            answer += 1
            if discount[start] in want:
                want_num[discount[start]] += 1
            start += 1
        # 그 외의 경우
        else:
            end += 1
            if end == len(discount): break
            if discount[end] in want:
                want_num[discount[end]] -= 1
    return answer

코딩테스트 연습 - 두 큐 합 같게 만들기

문제요약

길이가 동일한 큐 2개가 주어짐

하나의 큐에서 추출해서 다른 큐에 삽입함 : FIFO 방식으로 진행

추출+삽입 = 1회

두 큐의 합이 같아질 수 있는 최소 횟수 return

만일, 불가능하면 -1 return

입력

1≤ 각 q의 길이 ≤ 3*10^5

1≤ q의 원소 ≤ 10^9

출력

return int

불가능하면, return -1

어떻게 풀까?

  1. 일단, 입력 값의 범위를 봤을 때, O(n^2)는 불가능하고 O(nlogn) 정도까지는 가능
  2. 제일 먼저 예외처리를 할 수 있는 건, 두 큐의 합이 짝수가 아닐 때! 큐의 합을 모두 더해서 짝수가 아니면 바로 return -1을 해주자!
  3. 가장 간단하게 푸는 방법?
    while 문 돌리면서 각 큐의 합을 비교하고 큰 큐에서 값을 빼서 작은 큐에 넣어주는 것 반복 어차피 FIFO 방식이기 때문에, 결국 돌고 돌면서 크고 작음이 조율될 것!
    이때 문제는, 이게 얼마나 돌아야 return -1 판별이 가능한지임 사실 두 q 길이의 합 만큼 돌리면 원상복귀이기 때문에 더 이상 돌 필요가 없음! => 두 q 길이의 합은 2*3*10^5 로 충분히 가능!
  4. 이 때, 주의할 점은 deque로 만들어주고 해야한다는 점! 배열에서 바로 pop(0)을 해주면, O(N)이 걸리기 때문(배열에서 앞에걸 빼면 뒤에 원소들도 다 자리이동을 시키기 때문에)에 시간초과가 남 ⇒ deque popleft() 이용 : O(1)

내 코드

다르게 푸는 방법?

투포인터 활용하기!

def solution(queue1, queue2):
    target = (sum(queue1) + sum(queue2)) // 2
    cur = sum(queue1)
    queue3 = queue1 + queue2 + queue1

    s = 0
    e = len(queue1) - 1
    answer = 0
    while True:
        if cur == target:
            return answer
        if cur < target:
            e += 1
            if e >= len(queue3):
                return -1
            cur += queue3[e]
        else:
            cur -= queue3[s]
            s += 1
        answer += 1

" 위 풀이코드 출처 : algorithm-ps/두 큐 합 같게 만들기.md at main · dali0202/algorithm-ps "  

위 코드에서 유심히 볼 점!

단순히 q1+q2로 합치지 않고 q1+q2+q1를 해준 점

아래와 같은 경우가 있을 수 있음

 

Front-End Tesing이란?

  • GUI(Graphical User Interface), 웹 애플리케이션 또는 소프트웨어의 기능 및 유용성을 테스트하는 테스트 기술 (최종 사용자에게 표시되는 메뉴, 양식, 버튼 및 기타 애플리케이션 요소의 유효성 검사가 포함)
  • 프런트 엔드 테스트의 목표는 전반적인 기능을 테스트하여 웹 애플리케이션 또는 소프트웨어의 프레젠테이션 계층이 연속적인 업데이트로 결함이 없는지 확인하는 것
  • 위의 프런트엔드 테스트 외에도 다음을 위해 수행됨
    • CSS 회귀 테스트: 프런트엔드 레이아웃을 깨는 사소한 CSS 변경
    • 프런트엔드를 작동하지 않게 만드는 JS 파일 변경
    • 성능 확인

Front-End Test는 왜 할까?

안전한 프로덕션 배포(예: 해당 앱이 단순히 작동)를 보장하고 싶고, 사용자 상호 작용의 전체 주기 동안 애플리케이션이 안정적으로 유지되는지 확인하고 싶을 수도 있음

1. 클라이언트 측 성능 문제 감지

이것이 중요하지만 프런트 엔드 테스트를 통해 사용자 관점에서 서비스 테스트 가능

프런트 엔드 테스트를 통해 클라이언트 측의 문제를 정확히 확인하고 애플리케이션의 중요한 워크플로우의 안정성을 확인할 수 있음

사용성, 탐색 및 페이지 로드 속도와 같은 요소는 사용자와 검색엔진의 순위 알고리즘 모두에 중요

성능이 낮은 UI는 특히 중요한 워크플로가 손상된 경우 리드 또는 수익 창출 채널에 피해를 줄 수 있음

애플리케이션 프런트 엔드의 미묘한 오류가 돌이킬 수 없는 손상을 일으킬 수 있다는 것임

최종 사용자보다 먼저 시스템의 결함을 발견해야 하며 여기에서 프런트 엔드 테스트가 시작됨

즉, 무한 로딩 시간 또는 사용자가 종료할 수 없는 오류 상태와 같은 영역을 포함할 수 있는 애플리케이션의 취약하거나 중요한 부분에서 엣지 케이스를 보호해야 함

이러한 사항을 확인하기 위해 수행하는 테스트는 특정 요구 사항에 따라 수동 또는 자동일 수 있음

2. 다양한 브라우저 및 시스템에서 애플리케이션 동작 검증

프런트 엔드 테스트는 다양한 운영 체제, 브라우저 및 장치에서 웹 애플리케이션의 동작을 확인할 때 중요한 역할을 함

프런트 엔드를 테스트할 수 있는 수많은 브라우저와 OS 조합이 있음

다양한 시스템 아키텍처에서 애플리케이션의 기능 및 응답성을 검증하는 데 도움이 됨

이는 브라우저 기술의 수정과 결합된 클라이언트 측 개발의 발전으로 인해 호환성 문제가 발생할 수 있기 때문에 특히 중요함

따라서 프런트 엔드 테스트는 웹 사이트 또는 애플리케이션이 다른 장치 및 브라우저 엔진에서 동일하게 렌더링되는지 확인하는 데 필요

3. 사용자 상호 작용 및 경험의 품질 향상

프런트 엔드 테스트는 개발 팀이 이러한 성능 벤치마크를 최적화하여 사용자에게 더 나은 경험을 제공하는 데 도움이 됨

보다 구체적으로, 애플리케이션 로드 시간을 줄이고 애플리케이션의 콘텐츠가 올바르게 표시되도록 하며 다양한 장치 및 브라우저에서 인터페이스에 통합된 모양을 제공할 수 있음

이러한 클라이언트측 요소를 테스트하고 개선하면 애플리케이션의 품질이 기하급수적으로 향상될 수 있고 사용자는 다양한 환경에서 더 좋고 일관된 경험을 즐길 수 있을 것

4. 타사 서비스의 원활한 통합 보장

거의 모든 최신 애플리케이션은 어느 시점에서 타사 서비스와의 통합이 필요할 수 있음

특히 SaaS(Software as a Service) 플랫폼이 점차 인기를 얻고 있는 지금에는 그 가능성이 더 높음

애플리케이션에 다른 서비스를 통합할 때 성능이 좋지 않은 스크립트로 인해 손상될 수 있음

이는 사용자가 애플리케이션과 상호 작용할 때 사용자 경험에 상당한 피해를 줄 수 있기 때문에, 타사 서비스를 웹 애플리케이션에 통합하려는 사람에게는 프런트 엔드 테스트가 필수적임

5. 여러 개발자가 참여하는 프로젝트에서 코드 신뢰성 보장

최신 애플리케이션의 복잡성이 증가함에 따라 대규모 프로젝트를 단독으로 제공할 수 있는 경우는 거의 없음

누구도 다른 프로그래머가 작성한 모든 코드 조각에 대해 모든 것을 알 수는 없으며, 이것이 코드가 일관성이 없고 프런트엔드 기능이 손상되지 않았는지 다시 확인하는 프런트엔드 테스트가 중요한 이유임

6. 테스트 문서의 유효성

잘 관리된 테스트를 유지해야 하는 또 다른 좋은 이유는 테스트가 실제 문서로 제공되기 때문

테스트를 작성하려면 특정 테스트(및 관련된 애플리케이션의 구성 요소)가 수행하는 작업에 대한 적절한 설명이 필요합니다.

적절한 테스트를 실행하려면 구성 요소의 API를 사용하여 모의를 추가해야 하고, 나중에 다른 개발자나 팀이 나중에 어떻게 사용할 수 있는지에 대한 지침이 될 수 있음

7. 코드 가독성 향상 및 결합도를 낮출 수 있음

test suite를 작성하면 애플리케이션 코드의 가독성을 높이고 결합도를 낮출 수 있음

개발자가 응용 프로그램의 작은 청크를 테스트하고 싶지만 테스트와 반드시 관련되지 않은 몇 가지 종속 구성 요소 및 모의를 스핀업해야 하는 경우 이는 코드의 일부를 리모델링해야 한다는 신호일 수 있음 (⇒ 코드 부분 간의 상호 의존성이 너무 빡빡하다는 신호)

더 깨끗한 코드는 더 테스트하기 쉽고 테스트 가능한 코드는 더 깨끗함 ⇒ 이는 프런트엔드 개발자 와 궁극적으로 애플리케이션의 최종 사용자에게 윈-윈 시나리오임

결론

  • 프런트엔드 테스트는 프런트엔드 기능, GUI 및 사용성을 테스트하거나 확인하는 것
  • 프런트엔드 테스트의 주요 목표는 모든 사용자가 버그로부터 잘 보호되는지 확인하는 것
  • 프런트엔드 테스트 계획을 작성하면 프로젝트에서 다루어야 하는 장치, 브라우저 및 시스템을 파악하는 데 도움이 됨
  • 또한 프로젝트 범위에 대한 완전한 명확성을 얻는 데도 도움이 됨

Front End Testing 계획

테스트 시, 집중해야 할 몇 가지 측면

애플리케이션의 프런트엔드를 테스트할 때 집중해야 할 몇 가지 측면이 있음

  • 브라우저 간 및 플랫폼 간 기능
    • 다양한 브라우저, 플랫폼 및 장치에서 앱의 기능과 응답성을 모두 확인
  • 접근성
    • 시각 또는 청각 장애가 있는 사람을 포함하여 모든 사람이 애플리케이션에 액세스할 수 있는지 확인
  • end-to-end 확인
    • 사용자가 취할 가능성이 있는 실제 작업을 모방하여 애플리케이션의 end-to-end 워크플로(백엔드에서 프런트엔드로)를 확인하고 확인하는 데 필요
  • 이미지 분석 테스트
    • 요즘 대부분의 웹사이트와 앱에는 표준 디스플레이 이미지에서 로고, 인포그래픽 및 배너에 이르기까지 많은 이미지가 있음. 애플리케이션의 크기가 크게 증가하므로 테스트를 실행하여 앱이 더 빠르게 실행되도록 이미지를 최적화할 수 있는 위치를 확인해야 함
  • CSS(Cascading Style Sheets) 테스트
    • 두 가지 주요 CSS 요소인 구문 및 디스플레이의 성능을 보장하기 위해 테스트를 실행해야 함.

계획의 4단계

1단계) 테스트 계획 관리를 위한 도구 찾기

2단계) 프런트 엔드 테스트를 위한 예산 결정

3단계) 전체 프로세스의 타임라인 설정

4단계) 프로젝트의 전체 범위를 결정(범위에는 다음 항목 포함)

  • 사용자가 사용하는 OS 및 브라우저 사용자의 ISP (*ISP : 인터넷 서비스 제공자)
  • 사용자들이 많이 사용하는 기기
  • 사용자의 숙련도
  • 사용자의 인터넷 수정 속도

FIRST 원칙

프런트엔드 테스트는 중요하지만 테스트를 실행할 때 모범 사례를 보장하기 위해 특정 원칙을 고수하는 것도 중요

그렇지 않으면 테스트 결과를 완전히 신뢰하지 못할 수 있음

프런트엔드 테스트의 모범 사례를 고수하려면 따라야 할 프레임워크가 필요한데, FIRST 원칙을 사용가능함

FIRST 원칙은 다음을 의미합니다.

  • Fast : 빠른
  • Independent : 독립적인
  • Repeatable : 반복 가능한
  • Self-validating : 자체 검증 가능한
  • Thorough & Timely : 철저하고 적시에

테스트는 신속하게(수명 주기의 필요한 시점에서) 실행되어야 하고, 테스트되지 않은 구성 요소와 격리되어야 하며, 미래에 쉽게 반복할 수 있어야 하고, 테스트 통과 여부를 스스로 검증할 수 있어야 하며 필요한 모든 변수를 다룰 수 있어야 함

프런트엔드 요소의 우선 순위 지정

프런트엔드 테스트는 수백 또는 수천 개의 UI 및 기능 요소를 분석하고 확인하는 것을 의미

UI 요소에는 서식, CSS, 텍스트, 그래픽 등이 포함되며 기능 요소에는 양식, 링크, 버튼 등이 포함됨

효과적인 테스트 프로세스를 보장하려면 먼저 테스트할 항목의 우선 순위를 지정해야 함

페이지 로드 속도, 기본 텍스트, 이미지 및 필수 기능(예: 장바구니에 항목 추가, 결제 도구)을 먼저 테스트하고 그래픽 및 팝업으로 이동하기 전에 테스트하는 것이 합리적일 것

이러한 각 요소가 표시되고 반응하는지 확인한 다음 그래픽 및 레이아웃 확인으로 이동

실제 브라우저 및 장치 사용

실제 브라우저와 장치를 사용하는 것은 오류 없이 실제 환경을 최대한 반영하는 신뢰할 수 있는 프런트엔드 테스트를 수행하는 데 필수적임

에뮬레이터 및 시뮬레이터 사용을 피하고 실제 브라우저 및 장치를 사용하여 시간과 리소스를 절약하면 소프트웨어 테스트 결과를 훨씬 더 신뢰할 수 있음

테스트를 위한 팁

  • 예산, 자원 및 시간을 현명하게 준비
  • 테스트가 더 빨리 실행되도록 헤드리스 브라우저를 사용
  • 더 빠른 실행을 위해 테스트에서 DOM 렌더링의 양을 줄이기
  • 테스트 사례를 격리하여 버그의 근본 원인을 신속하게 파악하여 더 빠른 결함 수정 주기
  • 더 빠른 회귀 주기를 위해 테스트 스크립트를 재사용이 가능하게 만들기
  • 테스트 스크립트에 일관된 명명 규칙을 사용

프런트엔드 테스트 유형

프런트엔드에 대해 테스트할 여러 요소가 있으므로 실행을 고려할 수 있는 몇 가지 다른 유형의 테스트가 있고 이들 각각은 프런트엔드의 서로 다른 구성 요소에 초점을 맞추고 있음

단위 테스트

단위 테스트는 프런트엔드 테스트의 기본 빌딩 블록임

개별 구성 요소와 기능을 분석하여 예상대로 작동하는지 확인

이는 모든 프런트엔드 애플리케이션에 매우 중요하며 프로덕션 환경에서 예상되는 작동 방식에 대해 구성 요소 및 기능을 테스트하여 고객을 위한 안정적인 코드베이스와 신뢰할 수 있는 앱으로 이어짐

에지 케이스 및 테스트 API와 같은 항목에 단위 테스트를 사용할 수도 있음

Acceptance 테스트

수락 테스트는 사용자 입력, 사용자 흐름 및 프런트엔드의 지정된 작업이 코딩되고 제대로 작동하는지 확인하기 위해 수행됨

애플리케이션의 최종 모델이 최종 사용자가 기대하는 대로 작동하는지 확인하기 위해 이를 수행

Visual Regression(시각적 회귀) 테스트

시각적 회귀 테스트는 고유한 프런트엔드 테스트임

다른 유형의 테스트는 코드에 중점을 두므로 백엔드 스택에 대해서도 실행할 수 있음

차례로 시각적 회귀 테스트는 응용 프로그램의 실제/기존 인터페이스를 해당 '예상' 버전과 비교하여 차이를 식별함

이는 헤드리스, 서버 실행 브라우저의 스크린샷을 비교하여 이루어지며, 머신을 사용하여 스크린샷 간의 이미지 비교를 수행하고 차이점을 식별하고 강조 표시함

접근성 테스트

접근성 테스트는 시각 장애가 있거나 기타 추가 요구 사항이 있는 개인을 포함하여 모든 잠재적 사용자가 응용 프로그램이나 웹 사이트를 쉽게 사용할 수 있는지 확인함

때때로 사용성 테스트의 하위 범주로 간주되며 특정하고 변경 불가능한 조건으로 인해 앱의 기능에 액세스하는 데 방해가 되지 않고 다른 사람처럼 쉽게 인터페이스를 탐색할 수 있는지 확인함

성능 시험

성능 테스트는 속도, 안정성, 확장성, 상호 운용성 및 응답성을 포함한 특정 매개변수 내에서 애플리케이션의 성능을 분석함

사용자 로드가 증가할 때 제품이 원하는 품질을 유지하고 사용자 요청 및 작업에 빠르고 신속하게 응답하는지 확인하는 데 도움이 되므로 프런트엔드 테스트에 중요함

종단간(E2E) 테스트

종단 간 테스트는 응용 프로그램의 흐름이 처음부터 끝까지 예상대로 작동하는지 확인하고 확인하는 데 사용됨

주로 실제 시나리오 내에서 실제 사용자의 작업을 모방하여 응용 프로그램의 인터페이스와 API 간의 원활한 통신이 원활하게 실행되도록 함

이렇게 하면 함께 결합된 여러 시스템 요소의 결합된 동작에 대한 통찰력을 얻을 수 있음

통합 테스트

대부분의 최신 애플리케이션은 다양한 모듈로 구축됨

이러한 모듈이 제대로 통합되지 않고 함께 잘 작동하지 않으면 최종 사용자 경험을 망칠 수 있음

모든 것이 효과적으로 함께 작동하는지 확인하려면 통합 테스트를 실행해야 함

브라우저 간 테스트

브라우저 간 테스트는 응용 프로그램이 다른 웹 브라우저에서 예상대로 작동하는지 확인하기 위해 수행됨

이 프로세스에는 서로 다른 브라우저에서 동일한 테스트 케이스 세트를 실행하여 애플리케이션이 각 브라우저에서 호환되는지 확인하는 작업이 포함됨

이러한 테스트는 매번 동일하므로 이 프로세스를 자동화할 수 있음

Front-End 테스트 도구

Jest

Jest는 단순성에 중점을 둔 가장 인기 있는 JavaScript 테스트 프레임워크 중 하나

테스트에 고유한 전역 상태가 있는지 확인함으로써 Jest는 테스트를 병렬로 안정적으로 실행할 수 있음

작업을 빠르게 하기 위해 Jest는 이전에 실패한 테스트를 먼저 실행하고 테스트 파일이 걸리는 시간에 따라 실행을 재구성함

또한 강력한 코드 커버리지와 손쉬운 조롱 도구를 제공

Selenium WebDriver

Selenium WebDriver는 개발자가 브라우저 간 테스트를 실행할 수 있는 웹 프레임워크임

호환성을 확인하기 위해 웹 기반 애플리케이션 테스트를 자동화하는 데 사용

이 도구를 사용하면 프로그래밍 언어를 선택하여 브라우저 간 테스트를 위한 테스트 스크립트를 만들 수 있음

크로스 브라우저 테스트, 웹 테스트 및 웹 사이트의 올바른 기능이 확인되었는지 확인하는 데 효과적으로 사용할 수 있음

자동화된 스크립트는 다양한 플랫폼과 여러 브라우저에서 웹 애플리케이션용으로 사용자가 작성할 수 있음

많은 플러그인과 녹음 및 재생 솔루션을 제공하며, 브라우저와 직접 상호 작용하여 효율적이고 빠름

Cypress

Cypress는 웹 테스트 자동화를 위한 종단 간 테스트 프레임워크

단위 테스트, 통합 테스트, 종단 간 테스트와 같은 다양한 테스트를 효율적으로 작성할 수 있음

이를 통해 프런트엔드 개발자는 JavaScript로 자동화된 웹 테스트를 작성할 수 있음

Cypress Syntax의 사용편의성이 좋음

Cypress는 브라우저 내부에서 직접 작동할 수 있습니다. 브라우저의 동작을 수정할 수 있으며 인터페이스를 통해 오류를 쉽게 찾을 수 있음

WebDriverIO

WebdriverIO는 최신 웹 및 모바일 애플리케이션을 자동화하기 위해 구축된 진보적인 자동화 프레임워크임

앱과의 상호 작용을 단순화하고 확장 가능하고 강력하며 안정적인 test suite를 만드는 데 도움이 되는 플러그인 세트를 제공

NodeJS를 기반으로 구축되었으며 JavaScript 언어로 작성되었음.

간결한 스크립트 작성 기능이 제공되며 구조가 간단함

또한 타사 테스트 솔루션 제공업체와 쉽게 통합할 수 있음

친숙한 방식으로 프런트 엔드 테스트를 제공함

WebDriverJS

WebDriverJs는 Selenium의 Json-wire-protocol을 사용하여 브라우저와 상호 작용하는 Selenium의 공식 Javascript 버전임

WebDriverJS는 기본적으로 Selenium WebDriver와 동일한 기능을 수행함

Test Cafe

테스트 카페는 노드입니다. 웹 애플리케이션을 테스트하는 데 사용할 수 있는 Node.js 종단간 무료 오픈 소스 자동화 도구입니다. Windows, MacOS 및 Linux와 같은 널리 사용되는 모든 환경에서 작동합니다. 단일 명령의 설치하기 쉬운 기능을 사용하여 JavaScript 또는 TypeScript로 스크립트를 작성할 수 있습니다.

Lambda Test

가능한 모든 측면에서 웹 제품을 검사할 수 있도록 수많은 신규 및 레거시 모바일 및 데스크탑 브라우저와 OS를 제공하는 크로스 브라우저 테스트 도구

다양한 플랫폼에서 수동 테스트, 자동화 테스트, 지리적 위치 테스트, 사이프러스 테스트를 수행할 수 있으며 테스트 결과 스크린샷을 번거로움 없이 팀과 공유할 수 있음

팀 커뮤니케이션의 디버깅 및 품질 향상을 위해 많은 통합이 제공됨

Katalon Studio

모바일, 데스크톱 API 및 웹 UI 테스트를 제공하는 테스트 자동화 도구

테스트 생성은 다재다능하며 코딩 경험이 있거나 없는 사용자를 위한 이중 편집기 인터페이스와 함께 제공됨

여러 로케이터 전략으로 UI 변경을 완벽하게 조정할 수 있음

객체 탐지기의 불안정성을 처리하기 위해 자가 치유 메커니즘을 제공

각 실행 후 실시간 알림 및 통찰력 있는 그래프로 보고서를 생성할 수 있음

Test Complete

모바일, 웹 및 데스크톱 애플리케이션을 테스트하는 GUI 자동화 도구

비기술 사용자와 기술 사용자 모두 사용할 수 있음

애플리케이션의 품질은 효율성과 규모로 제공됨

코드리스 또는 코드 테스트 생성을 제공

복잡한 물체를 식별하고 뛰어난 물체 인식 기능을 제공

Front End 성능 최적화

이전의 성능 최적화는 서버 측 최적화를 의미했음

대부분의 웹사이트가 대부분 정적이었고 대부분의 처리가 서버 측에서 이루어졌기 때문

그러나 Web 2.0 기술의 시작과 함께 웹 애플리케이션이 더욱 동적으로 변하면서, 클라이언트 측 코드는 성능을 많이 차지하게 되었음

프런트 엔드 성능 최적화하면 뭐가 좋은데?

  • 웹 사이트 테스트에서 서버 병목 현상을 제외하고 클라이언트 측 성능 문제를 찾는 것은 사용자 경험에 쉽게 영향을 미치기 때문에 똑같이 중요
  • 백엔드 성능을 50% 향상시키면 애플리케이션의 전체 성능이 10% 향상되지만, 프런트 엔드 성능을 50% 향상시키면 애플리케이션의 전체 성능이 40% 향상됨
  • 또한 프런트엔드 성능 최적화는 백엔드보다 쉽고 비용 효율적임

예제를 통해 알아보자!

단위 테스트

단위 테스트는 테스트를 위한 가장 기본적인 빌딩 블록

개별 구성 요소를 살펴보고 예상대로 작동하는지 확인

이러한 종류의 테스트는 모든 프런트 엔드 애플리케이션에 중요

이를 통해 구성 요소가 예상되는 동작에 대해 테스트되어 훨씬 더 안정적인 코드베이스와 앱으로 이어지기 때문

이것은 또한 엣지 케이스와 같은 것을 고려하고 다룰 수 있는 곳임

단위 테스트는 API 테스트에 특히 유용함

그러나 라이브 API를 호출하는 대신 하드코딩된(또는 "모의") 데이터를 통해 테스트 실행이 항상 일관되게 유지됨

(간단한 예)

const sayHello = (name) => {
  if (!name) {
    return "Hello human!";
  }return `Hello ${name}!`;
};

이것은 기본적인 경우이지만 누군가가 애플리케이션에 이름을 제공하는 것을 무시했을 수 있는 작은 극단적인 경우를 다루고 있음

"왜 그렇게 작은 것을 테스트해야 하나?"

  • 당신의 기능의 가능한 결과에 대해 깊이 생각하도록 강요하며, 대부분의 경우 코드에서 이를 커버하는 데 도움이 되는 에지 케이스를 실제로 발견함
  • 코드의 일부는 이 엣지 케이스에 의존할 수 있으며 누군가 와서 중요한 것을 삭제하면 테스트는 이 코드가 중요하며 제거할 수 없다고 경고함

단위 테스트는 종종 작고 단순함

describe("sayHello function", () => {
  it("should return the proper greeting when a user doesn't pass a name", () => {
    expect(sayHello()).toEqual("Hello human!")
  })it("should return the proper greeting with the name passed", () => {
    expect(sayHello("Evgeny")).toEqual("Hello Evgeny!")
  })
})

describe테스트를 터미널에 인쇄되는 논리 블록 으로 it나눔

가장 중요한 줄은 expect및 toEqual임

이 expect함수는 유효성을 검사하려는 입력을 toEqual수락하고 원하는 출력을 수락함

응용 프로그램을 테스트하는 데 사용할 수 있는 다양한 기능과 메서드가 많이 있음

단위 작성을 위한 라이브러리인 Jest 로 작업하고 있다고 가정해 보았을 때, 위의 예에서 Jest는 sayHello함수를 터미널에 제목으로 표시함

함수 내부의 모든 것은 it단일 테스트로 간주되며 함수 제목 아래의 터미널에 보고되므로 모든 것을 매우 쉽게 읽을 수 있음

녹색 확인 표시는 두 테스트 모두 통과했음을 의미

통합 테스트

단위 테스트가 블록의 동작을 확인하는 경우 통합 테스트는 블록이 함께 완벽하게 작동하는지 확인

이는 구성 요소 간의 테스트 상호 작용을 열어주기 때문에 통합 테스트를 매우 중요하게 만듬

애플리케이션이 자체적으로 작동하는 분리된 부분으로 구성되는 경우는 매우 드뭄

이것이 우리가 통합 테스트에 의존하는 이유임

(간단한 예)

이번에는 간단한 React 애플리케이션으로 예시를 들어보자

버튼을 클릭하면 인사말이 화면에 표시된다고 가정해 보자

즉, 테스트에는 기능뿐만 아니라 HTML DOM 및 버튼의 기능도 포함됨

우리는 이 모든 부분이 어떻게 함께 작동하는지 테스트하고 싶음

<Greeting />테스트 중인 구성 요소 의 코드는 다음과 같음

export const Greeting = () => {
  const [showGreeting, setShowGreeting] = useState(false);return (
   <div>
     <p data-testid="greeting">{showGreeting && sayHello()}</p>
     <button data-testid="show-greeting-button" onClick={() => setShowGreeting(true)}>Show Greeting</button>
   </div>
 );
};

다음은 통합 테스트임

describe('<Greeting />', () => {
  it('shows correct greeting', () => {
    const screen = render(<Greeting />);
     const greeting = screen.getByTestId('greeting');
     const button = screen.getByTestId('show-greeting-button');expect(greeting.textContent).toBe('');
     fireEvent.click(button);
     expect(greeting.textContent).toBe('Hello human!');
 });
});

우리는 단위 테스트에서 describe이미 알고 있음

it테스트를 논리적 부분으로 나눔

특수하게 emulate된 DOM에  <Greeting /> 컴포넌트를 표시하는 render라는 기능이 있으므로 실제 DOM을 건드리지 않고 구성 요소와의 상호 작용을 테스트할 수 있음 (그렇지 않으면 비용이 많이 들 수 있음)

다음으로 테스트 ID( 및 )를 통한 테스트 쿼리 <p>및와 <button> 테스트 쿼리임

에뮬레이트된 DOM에서 원하는 구성 요소를 가져오는 것이 더 쉽기 때문에 테스트 ID를 사용함

7행이 되어서야 실제 통합 테스트가 시작됨

먼저 <p>태그가 비어 있는지 확인함

그런 다음 click이벤트를 시뮬레이션하여 버튼을 클릭함

마지막으로 <p>태그에 "Hello human!"이 포함되어 있는지 확인함

우리가 테스트하는 것은 버튼을 클릭한 후 빈 단락에 텍스트가 포함된다는 것임

물론 누군가 자신의 이름을 입력하는 input을 추가하고 인사말 기능에서 해당 input을 사용할 수 있음

통합 테스트를 실행할 때 터미널에는 아래와 같이 표시됨

<Greeting /> 요소는 단추를 클릭할 때 올바른 인사말을 표시함

종단간(E2E) 테스트

  • 레벨: 높음
  • 범위: 수행할 작업 및 예상 결과에 대한 지침을 제공하여 실제 브라우저에서 사용자 상호 작용을 테스트
  • 가능한 도구: Cypress, Puppeteer

E2E 테스트는 이 목록에서 가장 높은 수준의 테스트임

E2E 테스트는 사람들이 애플리케이션을 보는 방식과 애플리케이션과 상호 작용하는 방식에만 관심이 있음

그들은 코드와 구현에 대해 아무것도 모름

E2E 테스트는 브라우저에 수행할 작업, 클릭할 작업 및 입력할 내용을 알려줌

최종 사용자가 경험할 때 다양한 기능과 흐름을 테스트하는 모든 종류의 상호 작용을 만들 수 있음

문자 그대로 모든 것이 작동하는지 확인하기 위해 응용 프로그램을 클릭하기 위해 상호 작용하는 로봇임

E2E 테스트는 일종의 통합 테스트와 유사하지만, E2E 테스트는 실제 DOM이 있는 실제 브라우저에서 실행됨

우리는 일반적으로 이러한 테스트에서 실제 데이터와 실제 API로 작업함

단위 테스트와 통합 테스트를 모두 포함하는 것이 좋음

그러나 사용자는 브라우저에서 애플리케이션을 실행할 때 예기치 않은 동작에 직면할 수 있고 E2E 테스트는 이에 대한 완벽한 솔루션임

매우 인기 있는 테스트 라이브러리인 Cypress 를 사용하는 예를 살펴보자

다시 말하지만, 우리는 애플리케이션의 코드를 볼 필요가 없음

우리가 가정하고 있는 것은 우리에게 어떤 응용 프로그램이 있고 그것을 사용자로서 테스트하고 싶고 클릭할 버튼과 해당 버튼의 ID를 알고 있다는 것임

describe('Greetings functionality', () => {
  it('should navigate to greetings page and confirm it works', () => {
    cy.visit('<http://localhost:3000>')
    cy.get('#greeting-nav-button').click()
    cy.get('#greetings-input').type('Evgeny', { delay: 400 })
    cy.get('#greetings-show-button').click()
    cy.get('#greeting-text').should('include.text', 'Hello Evgeny!')
  })
})

이 E2E 테스트는 이전 통합 테스트와 매우 유사함

명령은 매우 유사하며 주요 차이점은 실제 브라우저에서 실행된다는 것임

먼저 cy.visit는 애플리케이션이 있는 특정 URL로 이동하는 데 사용함

cy.visit('<http://localhost:3000>')

둘째, cy.get로 ID로 탐색 버튼을 가져온 다음 테스트에 클릭하도록 지시함

해당 작업은 <Greetings/>구성 요소가 있는 페이지로 이동함

실제로 개인 웹사이트에 구성 요소를 추가하고 자체 URL 경로를 제공했음

cy.get('#greeting-nav-button').click()

그런 다음 순차적으로 텍스트 입력을 받고 "Evgeny"를 입력하고 #greetings-show-button버튼을 클릭하고 마지막으로 원하는 인사말 출력을 얻었는지 확인함

cy.get('#greetings-input').type('Evgeny', { delay: 400 })
cy.get('#greetings-show-button').click()
cy.get('#greeting-text').should('include.text', 'Hello Evgeny!')

무슨 일이 일어나고 있는지 볼 수 있도록 테스트 속도를 약간 늦췄지만, 이 모든 것은 일반적으로 매우 빠르게 발생함

터미널 출력은 다음과 같음

접근성 테스트

웹 접근성이란 웹 사이트, 도구 및 기술이 장애가 있는 사람들이 사용할 수 있도록 설계 및 개발되었음을 의미함 - W3C

접근성 테스트는 장애가 있는 사용자가 웹 사이트에 효과적으로 액세스하고 사용할 수 있는지 확인함

이 테스트는 접근성을 염두에 두고 웹 사이트를 구축하기 위한 표준을 준수하는지 확인함

예를 들어, 많은 시력이 없는 사람들은 스크린 리더를 사용함

스크린 리더는 웹사이트를 스캔하여 장애가 있는 사용자가 이해할 수 있는 형식(일반적으로 음성)으로 제공하려고 시도함

개발자는 접근성 테스트를 통해 스크린 리더의 작업을 쉽게 만들고 어디서부터 시작해야 하는지 이해하는 데 도움이 됨

다양한 도구 가 있으며 그 중 일부는 자동화되어 있고 일부는 접근성을 확인하기 위해 수동으로 실행됨

예를 들어 Chrome에는 이미 DevTools에 바로 내장된 도구가 하나 있고 그것은 Lighthouse 임

Lighthouse를 사용하여 E2E 테스트 섹션에서 만든 애플리케이션을 검증해 보자!

Chrome DevTools에서 Lighthouse를 열고 "접근성" 테스트 옵션을 클릭한 다음 보고서를 "생성"함

그게 문자 그대로 우리가 해야 할 전부임

Lighthouse는 작업을 수행한 다음 점수, 실행된 감사 요약 및 점수를 개선할 수 있는 기회 개요가 포함된 보고서를 생성함

그러나 이것은 특정 렌즈에서 접근성을 측정하는 하나의 도구일 뿐임

우리는 모든 종류의 접근성 도구 를 보유하고 있으며, 무엇을 테스트할지, 이러한 지점을 달성하는 데 사용할 수 있는 도구에 대한 계획을 세우는 것이 좋음

시각적 회귀 테스트

  • 레벨: 높음
  • 범위: 코드 변경으로 인한 시각적 차이를 포함하여 애플리케이션의 시각적 구조를 테스트함
  • 가능한 도구: Cypress,Percy,Applitools

때때로 E2E 테스트는 애플리케이션의 마지막 변경 사항이 인터페이스의 시각적 외관을 손상시키지 않았는지 확인하는 데 불충분함

대부분의 경우 코드베이스를 변경하면 앱의 시각적 구조 또는 레이아웃이 손상됨

해결책은 시각적 회귀 테스트임

작동 방식은 매우 간단함

시각적 테스트는 단순히 페이지 또는 구성 요소의 스크린샷을 찍어 이전의 성공적인 테스트에서 캡처한 스크린샷과 비교하고 이러한 테스트에서 스크린샷 간에 불일치가 발견되면 일종의 알림을 제공함

Percy 라는 시각적 회귀 도구를 사용하여 시각적 회귀 테스트가 어떻게 작동하는지 살펴보자!

시각적 회귀 테스트를 수행하는 다른 많은 방법이 있지만 Percy는 실제로 보여주기가 간단함

CSS-Tricks에서 Paul Ryan의 Percy 심층 분석으로 바로 이동할 수 있지만, 개념을 설명하기 위해 훨씬 더 간단한 작업을 수행해보자!

버튼을 입력 하단으로 이동하여 의도적으로 Greeting 애플리케이션의 레이아웃을 깨뜨렸을 때, Percy와 함께 이 오류를 잡아보자!

Percy는 Cypress와 잘 작동하므로 설치 가이드 를 따르고 기존 E2E 테스트와 함께 Percy 회귀 테스트를 실행할 수 있음

describe('Greetings functionality', () => {
  it('should navigate to greetings page and confirm everything is there', () => {
    cy.visit('<http://localhost:3000>')
    cy.get('#greeting-nav-button').click()
    cy.get('#greetings-input').type('Evgeny', { delay: 400 })
    cy.get('#greetings-show-button').click()
    cy.get('#greeting-text').should('include.text', 'Hello Evgeny!')// Percy test
     cy.percySnapshot()// HIGHLIGHT})
})

E2E 테스트 마지막에 추가한 것은 한 줄짜리임: cy.percySnapshot(). 이렇게 하면 스크린샷이 찍혀 비교를 위해 Percy에게 전송되고 그게 끝임

테스트가 끝나면 회귀를 확인할 수 있는 링크를 받게 됨

다음은 터미널에서 나타나는 것임

E2E 테스트도 통과한 것을 볼 수 있는데, 이는 E2E 테스트가 항상 시각적 오류를 포착하지 못하는 방법을 보여줌

Percy로부터 얻은 정보는 다음과 같음

분명히 변경된 사항이 있으며 수정해야 함

성능 시험

성능 테스트는 애플리케이션의 속도를 확인하는 데 유용함

비즈니스에 성능이 중요하고 Core Web Vitals 및 SEO에 중점을 둔  경우, 코드베이스의 변경 사항이 애플리케이션 속도에 부정적인 영향을 미치는지 확실히 알고 싶을 것임

이를 테스트 흐름의 나머지 부분에 적용하거나 수동으로 실행할 수 있음

이러한 테스트를 실행하는 방법과 실행 빈도는 전적으로 개발자에게 달려있음

일부 개발자는 "성능 예산" 을 만들고 앱 크기를 계산하는 테스트를 실행함

테스트에 실패하면 크기가 특정 임계값을 초과하는 경우 배포가 발생하지 않음

또는 성능 메트릭도 측정하므로 Lighthouse를 사용하여 수시로 수동으로 테스트하는 것을 추천함

또는 두 가지를 결합하여 Lighthouse를 테스트 도구 모음으로 빌드 할 수 있음

성능 테스트는 성능과 관련된 모든 것을 측정할 수 있음

애플리케이션이 얼마나 빨리 로드되는지, 초기 번들의 크기, 심지어 특정 기능의 속도까지 측정할 수 있음

성능 테스트는 다소 광범위하고 광활한 환경임

다음은 Lighthouse를 사용한 간단한 테스트를 해보자!

Core Web Vitals에 중점을 두고 설치나 구성 없이 Chrome의 DevTools에서 쉽게 액세스할 수 있기 때문에 확인이 용이함

 

부탁드리는 사항

혹시 잘못된 내용이나, 인용/차용 등에 있어 문제의 소지가 되는 내용이 있다면 언제든 알려주시면 큰 도움이 될 것 같습니다!

긴 글 읽어주셔서 감사합니다 :)

 

출처:
프론트엔드 테스트 정의 전반
https://www.guru99.com/frontend-testing.html
https://www.netguru.com/blog/front-end-testing
https://www.testim.io/blog/front-end-testing-complete-overview/
예시
https://css-tricks.com/front-end-testing-is-for-everyone/
https://lumiloves.github.io/2018/08/21/my-first-frontend-test-code-experience
테스트의 필요성
https://www.perfecto.io/blog/comprehensive-guide-front-end-testing
모범사례
https://meticulous.ai/blog/javascript-ui-testing-best-practices/
테스트 도구
https://www.rainforestqa.com/blog/automated-front-end-testing

 

Memoization?

1. 정의?

  • Memoization이란 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술
  • useMemo, useCallback, React.memo는 모두 이 Memoization을 기반으로 작동
  • 비용이 많이 드는 함수 호출의 결과를 저장하고 동일한 입력이 다시 발생할 때 캐시된 결과를 반환하여 컴퓨터 프로그램 속도를 높이는 데 주로 사용되는 최적화 기술
  • 소프트웨어 시스템의 일부 측면을 보다 효율적으로 작동시키거나 더 적은 리소스를 사용하도록 수정하는 프로세스

2. 최적화와 메모이제이션

  • 구성 요소의 수명 주기에서 React는 업데이트가 이루어질 때 구성 요소를 다시 렌더링함
    • 웹 페이지 하나가 만들어질 때는 위와 같이, DOM Tree의 구성, 레이아웃 잡기, 페인팅하기 등의 다양한 작업이 이루어짐
    • 리랜더링 시에, 레이아웃 및 페인팅 과정을 또 계산해야 할 수 있음
    ⇒ 그래서 React의 성능을 점검할 때는 컴포넌트 자체의 리랜더링이 불필요하게 반복되고 있지 않은지, 그리고 내부 로직이 쓸데없이 다시 만들어지거나 복잡한 계산을 반복하고 있지는 않은지에 대한 검토가 필요함
  • React가 구성 요소의 변경 사항을 확인할 때 JavaScript가 동등성 및 얕은 비교(equality and shallow comparisons)를 처리하는 방식으로 인해 의도하지 않거나 예기치 않은 변경 사항을 감지할 수 있고 React 애플리케이션은 이러한 변경으로 인해 불필요하게 재렌더링될 수 있음

⇒ 비용이 많이 드는 작업은 시간, 메모리 또는 처리 비용이 많이 들 수 있어 성능저하가 발생할 수 있으므로 사용자 경험 또한 저하될 수 있음

⇒ React는 이를 개선하기 위해 메모 아이디어를 발표함

⇒ 쓸데없이 같은 계산을 반복하게 하지 않게 할 수 있는 방법은? 결과를 기억하는 것

useMemo

1. 이건 뭐야?

정의?

  • 이전 값을 기억해두었다가 조건에 따라 재활용하여 성능을 최적화 하는 용도로 사용됨 (특정 value를 재사용)
  • useMemo는 함수의 결과 값을 memoized하여 불필요한 연산을 관리
  • 사용 형식
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • useMemo의 특징은 일단 함수 호출 이후의 return 값이 memoized되며, 두 번째 파라미터인 배열의 요소가 변경될 때마다 첫 번째 파라미터의 callback 함수를 다시 생성하는 방식임

useRef와의 차이

useMemo는 deps가 변경되기 전까지 값을 기억하고, 실행후 값을 보관하는 역할로도 사용

  • useMemo는 복잡한 함수의 return 값을 기억한다는 점에서 값만 기억하는 useRef와는 다름
  • useRef는 특정 값을 기억하는 경우, useMemo는 복잡한 함수의 return값을 기억하는 경우에 사용됨

동작방식

  • 초기 렌더링 중에 useMemo(compute,dependencies)계산을 호출하고 계산 결과를 메모한 다음 구성 요소로 반환함
  • useMemo종속성 중 하나가 변경된 경우에만 메모된 값을 다시 계산하며, 이 최적화는 모든 렌더링에서 비용이 많이 드는 계산을 피하는 데 도움이 됨
  • 다음 렌더링 중에 종속성이 변경되지 않으면 useMemo() 는 컴퓨팅을 호출하지 않고 메모된 값을 반환함

2. 어디에 써?

  • 비용이 많이 드는 계산을 메모화하는 데 사용
    • 여기서 비싸다는 의미는 메모리와 같은 리소스를 많이 사용한다는 것을 의미

3. 주의점은?

종속성 비교로 인한 계산 비용

  • 내부적으로 React의 useMemo Hook은 값을 다시 계산해야 하는지 여부를 결정하기 위해 다시 렌더링할 때마다 종속성 배열의 종속성을 비교해야 하며, 종종 이 비교를 위한 계산은 단순히 값을 다시 계산하는 것보다 비용이 더 많이 들 수 있음 ⇒ useMemo애플리케이션에서 너무 자주 구현 하면 성능이 저하될 수 있음
  • 프로파일링 도구를 사용하여 비용이 많이 드는 성능 문제를 식별할 수 있음

4. 예시를 살펴보자!

useMemo 사용 전

import { useState } from 'react';

export function MyComponent() {
  const [number, setNumber] = useState(1);
  const [inc, setInc] = useState(0);

  const factorialResult = calculateFactorial(number);
  const onChange = event => {
    setNumber(Number(event.target.value));
  };
  const onClick = () => setInc(i => i + 1);

  return (
    <div>
      Factorial of the following Number
      <input type="number" value={number} onChange={onChange} />
      is {factorialResult}
      <button onClick={onClick}>Increment</button> <span>{inc}</span>
    </div>
  );
}

function calculateFactorial(number) {
  console.log('calculateFactorial called!');
  return number <= 0 ? 1 : number * calculateFactorial(number - 1);
}
  1. 입력 값을 변경할 때마다 factorialResult가 계산 'calculateFactorial(number) called!'되어 콘솔에 기록됨
  2. Increment 버튼을 클릭할 때마다 inc상태 값이 업데이트됩니다. 상태 값을 업데이트 하면 다시 렌더링 inc이 트리거됨
  3. <MyComponent /> 가 2.번의 이벤트로 인해, 재렌더링되는 동안 다시 calculateFactorial계산 되어 'calculateFactorial(n) called!'값이 콘솔에 기록됨

⇒ useMemo(()=> calculateFactorial(number), [number]) 으로 React는 계산값을 메모할 수 있음

useMemo 사용 후

import { useState, useMemo } from 'react';

export function MyComponent() {
  const [number, setNumber] = useState(1);
  const [inc, setInc] = useState(0);

  const factorialResult = useMemo(() => calculateFactorial(number) , [number]);
  const onChange = event => {
    setNumber(Number(event.target.value));
  };
  const onClick = () => setInc(i => i + 1);

  return (
    <div>
      Factorial of the following Number
      <input type="number" value={number} onChange={onChange} />
      is {factorialResult}
      <button onClick={onClick}>Increment</button> <span>{inc}</span>
    </div>
  );
}

function calculateFactorial(number) {
  console.log('calculateFactorial called!');
  return number <= 0 ? 1 : number * calculateFactorial(number - 1);
}
  • 입력(input) 값을 변경할 때마다 'calculateFactorial(n) called!'가 콘솔에 기록되지만, Increment 버튼을 클릭 하면 useMemo 에 의해, 메모된 계산값이 반환되기 때문에, 'calculateFactorial(n) called!' 은 콘솔에 기록되지 않음

useCallback

1. 이건 뭔데?

  • useCallback은 리액트의 렌더링 성능을 위해서 제공되는 Hook이다.
  • 부모컴포넌트에서 자식컴포넌트에 prop으로 넘겨주는 함수가 있을 때, 부모 컴포넌트가 렌더링 될 때마다 내부적으로 사용된 함수도 새로 생성되어, 자식 컴포넌트에 Prop으로 새로 생성된 함수가 넘겨지게 되면 불필요한 리렌더링이 일어날 수 있다.
  • ⇒ 이 경우, 함수를 memoized하여 해결할 수 있음
  • 메모리제이션된 함수를 반환하는 것이 핵심
  • 사용형식
  • const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
  • useCallback을 사용하여 함수를 memoized 시키면, 종속성 배열의 종속성이 변경되는 경우에만 이 함수가 다시 정의됨
  • useCallback의 특징
  1. useCallback은 function의 메모리 재할당을 막기위한 수단
  2. 여러곳에서 사용되는 컴포넌트가 불필요하게 같은 function을 메모리에 여러번 할당한다면, useCallback을 사용한 최적화가 필요
  3. useCallback은 함수의 결과를 메모리에 저장하는게 아니라, 메모리에 저장된 함수를 같은 컴포넌트들에서 공유하는 개념

2. 언제 써?

  • 함수를 메모하기 위해 사용되며, 부모 구성 요소를 다시 렌더링할 때마다 함수가 다시 초기화되는 것에 대해 걱정하지 않고 다른 구성 요소에 함수를 전달할 때 이미 약간의 성능 향상이 있음
  • useCallback은 React.Memo와 함께 사용할 때 특히 유용함
  • 컴포넌트가 랜더링 될 때마다 내부에 선언되어 있던 표현식이 다시 선언되어 사용됨. 이 때, 컴포넌트 내부에 있는 함수는 변동이 없음에도 컴포넌트가 리랜더링 될 때마다 다시 선언됨. ⇒ 이런 경우에 useCallback을 import해서 사용하던 함수의 실행문을 넣어주면 랜더링 될 때마다 선언되는 것을 피할 수 있고 의존성 배열에 요소를 추가하면 해당 값이 변경될 때 재선언 가능.
  • 또한, 상위컴포넌트의 함수가 매번 재선언되면, 내용이 같다고 하더라도 하위컴포넌트는 넘겨받는 함수가 달라졌다고 인식함. ⇒ 따라서 하위컴포넌트가 React.memo() 등으로 최적화 되어있고, 그 하위 컴포넌트에게 callback 함수를 props로 넘길 경우에, 상위컴포넌트에서 useCallback으로 선언하는 것이 최적화에 도움됨.
    • React.memo()로 함수형 컴포넌트 자체를 감싸면 넘겨 받는 props가 변경되지 않았을 때는 상위 컴포넌트가 메모리제이션된 함수형 컴포넌트(이전에 렌더링된 결과)를 사용하게 됨.

3. 주의점은?

4. 사용 예시를 살펴보자

React.memo로 래핑된 컴포넌트가 callback을 받을 때

const MemoisedItem = React.memo(Item);
const List = () => {
**// this HAS TO be memoised, otherwise `React.memo` for the Item is useless**
  const onClick = () => {console.log('click!')};
  return <MemoisedItem onClick={onClick} country="Austria" />
}

  • 함수 객체는 "일반" 객체와 동일한 비교 원칙을 따름 함수 객체는 오직 자신에게만 동일
  • 몇가지 함수를 비교해보자.
function sumFactory() {
  return (a, b) => a + b;
}

const sum1 = sumFactory();
const sum2 = sumFactory();

console.log(sum1 === sum2); // => false
console.log(sum1 === sum1); // => true
console.log(sum2 === sum2); // => true
  • sumFactory()는 팩토리 함수이다. 이 함수는 2가지 숫자를 더해주는 화살표 함수를 반환
  • 함수 sum1과 sum2는 팩토리에 의해 생성된 함수이고, 두 함수 모두 두 숫자를 더해주는 함수임. 그러나 sum1과 sum2는 다른 함수 객체임.
  • 부모 컴퍼넌트가 자식 컴퍼넌트의 콜백 함수를 정의한다면, 새 함수가 암시적으로 생성될 수 있음.
  • 예시를 통해 알아보자.
  • Logout 컴퍼넌트는 콜백 prop인 onLogout을 갖는다.
function Logout({ username, onLogout }) {
  return <div onClick={onLogout}>Logout {username}</div>;
}

const MemoizedLogout = React.memo(Logout);
  • 함수의 동등성이란 함정 때문에, 메모이제이션을 적용할 때는 콜백을 받는 컴퍼넌트 관리에 주의해야함.
  • 리렌더를 할 때 마다 부모 함수가 다른 콜백 함수의 인스턴스를 넘길 가능성이 있음.
function MyApp({ store, cookies }) {
  return (
    <div className="main">
      <header>
        <MemoizedLogout
          username={store.username}
          onLogout={() => cookies.clear()}
        />
      </header>
      {store.content}
    </div>
  );
}
  • 동일한 username 값이 전달되더라고, MemoizedLogout은 새로운 onLogout 콜백 때문에 리렌더링을 하게 됨.
  • ⇒ 메모이제이션이 중단되게 되는 것
  • 이 문제를 해결하려면 onLogout prop의 값을 매번 동일한 콜백 인스턴스로 설정해야만 함. useCallback()을 이용해서 콜백 인스턴스를 보존시킬 수 있음.
const MemoizedLogout = React.memo(Logout);

function MyApp({ store, cookies }) {
  const onLogout = useCallback(() => {
    cookies.clear();
  }, []);
  return (
    <div className="main">
      <header>
        <MemoizedLogout username={store.username} onLogout={onLogout} />
      </header>
      {store.content}
    </div>
  );
}
  • useCallback(() => { cookies.clear() }, []) 는 항상 같은 함수 인스턴스를 반환하고,  MemoizedLogout의 메모이제이션이 정상적으로 동작하도록 수정되었음

컴포넌트가 hooks(useMemo, useCallback or useEffect)에 dependency로 callback을 받을 때

const Item = ({ onClick }) => {
  useEffect(() => {
// some heavy calculation here
    const data = ...
    onClick(data);
**// if onClick is not memoised, this will be triggered on every single render**
  }, [onClick])
  return <div>something</div>
}
const List = () => {
// this HAS TO be memoised, otherwise `useEffect` in Item above
// will be triggered on every single re-render
  const onClick = () => {console.log('click!')};
  return <Item onClick={onClick} country="Austria" />
}

나쁜 사용 사례

import { useCallback } from 'react';
function MyComponent() {
  // Contrived use of `useCallback()`
  const handleClick = useCallback(() => {
    // handle the click event
  }, []);
  return <MyChild onClick={handleClick} />;
}
function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

  • 첫 번째 문제는 렌더링 useCallback()할 때마다 후크가 호출 된다는 것 : 그것은 이미 렌더링 성능을 감소시킴
  • 두 번째 문제는 사용 useCallback()이 코드 복잡성을 증가시키는 것 : useCallback(..., deps) 의 deps와 memoized 콜백 내에서 사용 중인 것과 동기화 deps를 유지해야 함.
  • useCallback()의미가 있을까? : <MyChild>구성 요소가 가볍고 다시 렌더링해도 성능 문제가 발생하지 않기 때문일 가능성이 높음

⇒ 결론적으로 최적화를 하지 않는 것보다 최적화 비용이 더 많이 듬

React.memo

1. 이게 뭔데?

  • UI 성능을 증가시키기 위해, React는 고차 컴포넌트(Higher Order Component, HOC) React.memo()를 제공
    • 고차 컴포넌트
      • 고차 컴포넌트(HOC, Higher Order Component)는 컴포넌트 로직을 재사용하기 위한 React의 고급 기술
      • 고차 컴포넌트(HOC)는 React API의 일부가 아니며, React의 구성적 특성에서 나오는 패턴
      • 구체적으로, 고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수
  • 렌더링 결과를 메모이징(Memoizing)함으로써, 불필요한 리렌더링을 건너뜀
  • 컴포넌트가 동일한 props로 동일한 결과를 렌더링해낸다면, React.memo를 호출하고 결과를 메모이징(Memoizing)하도록 래핑하여 경우에 따라 성능을 향상시킬 수 있음 ⇒ React.memo는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용함
  • React.memo는 props 변화에만 영향을 주며, React.memo로 감싸진 함수 컴포넌트 구현에 useState, useReducer 또는 useContext 훅을 사용한다면, 여전히 state나 context가 변할 때 다시 렌더링됨
    • 사용형태
    const MyComponent = React.memo(function MyComponent(props) {
      /* props를 사용하여 렌더링 */
    });
    
    • (간단한 예) React.memo는 일반적으로 아래와 같이 사용됨
      • React.memo는 Welcome의 결과를 Memoization해서 이후 props가 변경될때까지 현재 memoized된 내용을 그대로 사용하여 리렌더링을 막음
      • ⇒ 이렇게 Memoized된 내용을 재사용하여 렌더링시 가상 DOM에서 달라진 부분을 확인하지 않아 성능상의 이점이 생기게 됨
    const Welcome = ({ name }) => {
      return <h1>Hello { name }</h1>;
    };
    
    export default React.memo(Welcome);
    
  • props가 갖는 복잡한 객체에 대하여 얕은 비교만을 수행하는 것이 기본 동작이며, 다른 비교 동작을 원한다면, 두 번째 인자로 별도의 비교 함수를 제공하면 됨.
    • 얕은 비교 : 원시 값의 경우는 같은 값을 갖는지 확인하고 객체나 배열과 같은 참조 값은 같은 주소 값을 갖고 있는지 확인
function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);
  • (간단한 예) Movie의 props가 동일한지 수동으로 비교해보자.
    • moviePropsAreEqual() 함수는 이전 props와 현재 props가 같다면 true를 반환할 것
function moviePropsAreEqual(prevMovie, nextMovie) {
  return (
    prevMovie.title === nextMovie.title &&
    prevMovie.releaseDate === nextMovie.releaseDate
  );
}

const MemoizedMovie2 = React.memo(Movie, moviePropsAreEqual);

2. 어디에 쓸까?

사용처

  1. Pure Functional Component(동일한 상태 및 props에 대해 동일한 출력을 렌더링하는 컴포넌트)에서 Rendering이 자주일어날 경우
  2. re-rendering이 되는 동안에도 계속 같은 props값이 전달될 경우
  3. UI element의 양이 많은 컴포넌트의 경우
  4. 동일한 props에 항상 같은 것을 랜더링하는데, 같은 props로 랜더링이 자주 일어날 때(즉, 일부 데이터를 가져오기 위해 네트워크 호출을 해야 하고 데이터가 동일하지 않을 가능성이 있는 경우 사용 지양)

결론

  • 컴퍼넌트가 무겁고 비용이 큰 연산이 있는데 같은 props로 자주 렌더링되거나 같은 props로 리랜더링이 엄청 자주 일어난다면  React.memo()로 컴퍼넌트를 래핑하면 좋음
    (단, 동일한 props로는 항상 같은 리랜더링 결과가 나올 때)

3. 주의점은?

일반적인 주의사항

  • 이 메서드는 오직 **성능 최적화**를 위하여 사용되므로 렌더링을 “방지”하기 위하여 사용할 경우, 버그가 생성될 수 있음
  • 최적화를 위한 연산이 불필요한 경우엔 비용만 발생시키기 때문에 무조건적인 사용은 지양
  • React.memo는 Props의 변경 사항만 확인하므로, React.memo에 래핑된 함수 컴포넌트 요소에 useState , useReducer 또는 useContext Hook이 있는 경우 상태 또는 컨텍스트가 변경될 때 여전히 다시 렌더링됨

부모가 전달하는 callback 함수에 대한 주의사항

  • useCallback과 함께 사용할 때의 예제

useCallback 사용 전

function CountButton({ onClick, count }) {
  return <button onClick={onClick}>{count}</button>;
}
function DualCounter() {
  const [count1, setCount1] = React.useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = React.useState(0);
  const increment2 = () => setCount2(c => c + 1);
  return (
    <>
      <CountButton count={count1} onClick={increment1} /> // React.memo로 래핑되었다는 가정
      <CountButton count={count2} onClick={increment2} /> // React.memo로 래핑되었다는 가정
    </>
  );
}

useCallback 사용 후

const CountButton = React.memo(function CountButton({ onClick, count }) {
  return <button onClick={onClick}>{count}</button>;
});
function DualCounter() {
  const [count1, setCount1] = React.useState(0);
  const increment1 = React.useCallback(() => setCount1(c => c + 1), []);
  const [count2, setCount2] = React.useState(0);
  const increment2 = React.useCallback(() => setCount2(c => c + 1), []);
  return (
    <>
      <CountButton count={count1} onClick={increment1} /> // React.memo로 래핑되었다는 가정
      <CountButton count={count2} onClick={increment2} /> // React.memo로 래핑되었다는 가정
    </>
  );
}
  • state count1이 변경되었을 때, state 변경이 없었던 count2를 참조하는 CountButton 컴포넌트는 리렌더리 되지 않아야 함(React.memo로 래핑되었다는 가정)
  • 만약 increment2 함수에 useCallback이 없었다면, DualCounter 컴포넌트는 state의 변경으로 인해 re-rendering 될 것이고, increment1과 increment2 함수 모두 새로 생성되어 2개의 CountButton 컴포넌트는 모두 re-rendering 될 것
  • 하지만 increment1, increment2 함수에 useCallback을 사용함으로써 두개의 함수는 재 생성이 되지 않고 (종속배열도 비어있음) 변경된 count1을 참조하는 CountButton만 re-rendering 되게 됨

4. 예시로 알아보자!

  • 같은 props로 렌더링이 자주 일어나는 컴퍼넌트에 사용하기 좋음
    • React.memo()를 사용하기 가장 좋은 케이스는 함수형 컴퍼넌트가 같은 props로 자주 렌더링 될거라 예상될 때이다.
    • 일반적으로 부모 컴퍼넌트에 의해 하위 컴퍼넌트가 같은 props로 리렌더링 될 때가 있음
  • Movie의 부모 컴퍼넌트인 실시간으로 업데이트되는 영화 조회수를 나타내는 MovieViewsRealtime 컴퍼넌트가 있다고 하자.
function MovieViewsRealtime({ title, releaseDate, views }) {
  return (
    <div>
      <Movie title={title} releaseDate={releaseDate} />
      Movie views: {views}
    </div>
  );
}
  • 이 어플리케이션은 주기적(매초)으로 서버에서 데이터를 폴링(Polling)해서 MovieViewsRealtime 컴퍼넌트의 views를 업데이트함
// Initial render
<MovieViewsRealtime views={0} title="Forrest Gump" releaseDate="June 23, 1994"/>// After 1 second, views is 10
<MovieViewsRealtime views={10} title="Forrest Gump" releaseDate="June 23, 1994"/>// After 2 seconds, views is 25
<MovieViewsRealtime views={25} title="Forrest Gump" releaseDate="June 23, 1994"/>// etc
  • views가 새로운 숫자가 업데이트 될 때 마다 MoviewViewsRealtime 컴퍼넌트 또한 리렌더링 되며, Movie 컴퍼넌트 또한 title이나 releaseData가 같음에도 불구하고 리렌더링 됨
  • 이때가 Movie 컴퍼넌트에 메모이제이션을 적용할 적절한 케이스임
  • MovieViewsRealtime에 메모이징된 컴퍼넌트인 MemoizedMovie를 대신 사용해 성능을 향상해보자.
function MovieViewsRealtime({ title, releaseDate, views }) {
  return (
    <div>
      <MemoizedMovie title={title} releaseDate={releaseDate} />
      Movie views: {views}
    </div>
  );
}
  • title 혹은 releaseDate props가 같다면, React는 MemoizedMovie를 리렌더링 하지 않을 것이다. 이렇게 MovieViewsRealtime 컴퍼넌트의 성능을 향상할 수 있음

React.memo vs. useMemo vs. useCallback

1. 공통점

공통점

  • React.memo, useMemo, useCallback은 모두 불필요한 렌더링 또는 연산을 제어하는 용도로 성능 최적화에 그 목적이 있음
  • 재렌더링 사이의 메모이제이션임
  • 전달하려는 항목이 새로운 참조여도 상관없다면, 사용하지 말아야 한다. 매번 새로운 참조여도 상관없는데, 새로운 참조라면 메모이제이션하는 것이 의미가 없음

useMemo와 useCallback을 사용해야 하는 경우

  1. 하위트리에 많은 Consumer가 있는 값을 Context Provider에 전달해야 하는 경우 useMemo를 사용하는 것이 좋음  <ProductContext.Provider value={{id, name}} >의 경우, 어떤 이유로든 해당 컴포넌트가 리렌더링 된다면 id  name이 동일하더라도 매번 새로운 참조를 만들어 죄다 리렌더링 될 것
  2. 계산 비용이 많이 들고, 사용자의 입력 값이 렌더링 이후로도 참조적으로 동일할 가능성이 높은 경우, useMemo를 사용하는 것이 좋음
  3. 매우 큰 리액트 트리 구조 내에서, 부모가 리렌더링 되었을 때 이에 다른 렌더링 전파를 막고 싶을 때 사용하자. 자식 컴포넌트가 React.memo  React.PureComponent일 경우, 메모이제이션된 props를 사용하게되면 딱 필요한 부분만 리렌더링 될 것

사용팁

React DevTools Profiler를 사용하면 컴포넌트의 리렌더링 속도가 느린 경우, 상태 변경이 일어났을 때 얼마나 렌더링 시간이 걸렸는지 조사할 수 있음

이렇게 하면 거대한 계단식 리렌더링을 방지하기 위해 React.memo를 사용할 위치를 찾을 수 있고, 필요한 경우 useCallback useMemo를 사용하여 상태변경을 더 효율적으로 만들 수 있음

2. 차이점

  • React.memo는 HOC이고, useMemo와 useCallback은 hook
  • React.memo는 HOC이기 때문에 클래스형 컴포넌트, 함수형 컴포넌트 모두 사용 가능하지만, useMemo는 hook이기 때문에 함수형 컴포넌트 안에서만 사용 가능
  • useMemo는 함수의 연산량이 많을때 이전 결과값을 재사용하는 목적이고, useCallback은 함수가 재생성 되는것을 방지하기 위한 목적(React.memo와 useMemo의 차이는 어디에 활용되는가임)
  • React.memo의 경우에는 컴포넌트를 받아 컴포넌트를 반환한다.
  • useMemo의 경우에는 값을 계산하는 과정을 최적화해 값을 반환받음(컴포넌트도 값이기에 useMemo 안에 넣을 수 있음)

3. 주의점

  • 일부 개발자가 흔히 저지르는 실수는 성능 문제를 방지하기 위해 필요하지 않은 경우에도 이러한 후크(및 기타 최적화 기술)를 사용하는 것임
  • 이는 코드를 더 복잡하게 만들고(따라서 유지 관리하기 더 어렵게 만들고) 경우에 따라 성능이 더 나빠지기 때문에 권장되지 않음
  • 성능 문제를 찾은 후 이러한 기술을 적용해야 함
  • 원하는 만큼 빠르게 실행되지 않는 경우 병목 현상이 있는 부분을 조사하고 해당 부분을 최적화가 필요
  • useCallback을 사용하여 접근하는 좋은 방법은 능동적이기보다는 반응적으로 접근하는 것임
  • 즉, 구성 요소에 따라 조급한 성능 최적화가 아니라 분명히 필요할 때 사용하는 것이 중요함
  • useCallback의 함수 본문 내부에 있는 모든 함수를 래핑하지 않도록 하자.

React Devtools Profile

React Devtools로 프로파일링

  1. 먼저 React Devtools 브라우저 확장 프로그램을 다운로드해야 함
    1. 현재 Chrome 및 Firefox 에서 사용할 수 있음
    2. 여기서는 Chrome을 사용한다고 가정하지만, 방법은 크게 다르지 않음

  1. "Profiler" 탭을 선택
  2. 작은 기어 아이콘을 클릭하고 "프로파일링 중 각 구성 요소가 렌더링된 이유 기록" 옵션을 활성화함
  • 일반적인 흐름은 다음과 같음
    1. 작은 파란색 "녹음" 원을 눌러 녹음 시작
    2. 애플리케이션에서 몇 가지 작업을 수행
    3. 녹음을 중지
    4. 기록된 스냅샷을 보고 무슨 일이 일어났는지 확인 가능
  • 각 렌더링은 별도의 스냅샷으로 캡처되며 화살표를 사용하여 탐색할 수 있음
    (참조) https://storage.googleapis.com/joshwcomeau/devtools-demo-v2.mp4
  • 관심 있는 구성 요소를 클릭하면 특정 구성 요소가 다시 렌더링된 이유를 정확하게 확인할 수 있음.
  • React 프로파일러에는 다시 렌더링하는 구성 요소를 강조 표시할 수 있는 옵션이 있음

 

이 설정을 사용하면 다시 렌더링하는 구성 요소 주위에 녹색 사각형이 깜박이는 것을 볼 수 있고 이를 통해 상태 업데이트가 얼마나 광범위한지 확인할 수 있고 일부 요소가 재렌더링을 성공적으로 피하는지 테스트할 수 있음

 

개인적으로 느낀 점

일전에 React를 사용하며, 부모 Component에서 자식 Component로 callback을 Prop으로 내려줬는데 의도치않게 너무 많은 랜더링이 일어나는 이슈를 겪은 적이 있다.

그 때, useCallback으로 해결한 경험이 있어서 그 이후 useCallback을 남발하게 되었던 것 같다.

메모이제이션이라는 것이 어딘가에 저장을 하는 만큼(메모리) 결코 공짜가 아니라는 생각이 들었고 과연 나는 useCallback을 효율적으로 사용하고 있는가라는 의문이 들었다.

관련하여 찾다보니, React의 다른 메모이제이션 훅, HOC에 대해서도 찾을 수 있었다.

역시, 메모이제이션 기능은 공짜가 아니었고 오히려 이러한 성급한 최적화 시도가 성능을 더 저하시킬 수 있다는 것을 확인하였다.

useCallback의 경우에도 꼭 필요한 경우(React.memo로 래핑한 자식 컴포넌트에 callback을 넘겨주는 경우, 자식 컴포넌트로 내려가는 callback으로 인해, 자식의 useEffect가 의도치 않게 계속 시행되는 경우 등)와 사용에 대한 근거 없이는 사용을 자제해야겠다는 생각을 했다.

이전에 너무 많은 랜더링이 일어난 상황이 현재는 잘 기억나진 않지만, 아마도 useEffect 종속성 문제와 겹치면서 일어난 참사이지 않았을까 생각이 든다.

당시 React.memo를 사용하는 상황은 아니었기 때문에 굳이 useCallback을 쓰지 않고 해결할 수 있는 방법도 있지 않았을까 생각이 들고 상황상 여의치 않다면 이전에 해결한 방법과 동일하게 useCallback을 결국 써야했을 것 같다.

 

부탁드리는 사항

혹시 잘못된 내용이나, 인용/차용 등에 있어 문제의 소지가 되는 내용이 있다면 언제든 알려주시면 큰 도움이 될 것 같습니다!

긴 글 읽어주셔서 감사합니다 :)

 

출처:
메모이제이션 정의
https://ko.wikipedia.org/wiki/메모이제이션
useMemo
https://www.digitalocean.com/community/tutorials/react-usememo
https://ko.reactjs.org/docs/react-api.html React.Memo
https://ui.toast.com/weekly-pick/ko_20190731
Pure Functional Component
https://blog.logrocket.com/what-are-react-pure-functional-components/
전반적 설명 및 예시
https://www.joshwcomeau.com/react/usememo-and-usecallback/
https://medium.com/geekculture/great-confusion-about-react-memoization-methods-react-memo-usememo-usecallback-a10ebdd3a316
전반적 역할 설명 및 비교
https://medium.com/hcleedev/web-최적화와-react-memo-usememo-알아보기-4324a237a039 https://velog.io/@sunkim/React.memo-useMemo-useCallback-역할-및-차이점 https://ssangq.netlify.app/posts/react-memo-useMemo-useCallback
https://www.developerway.com/posts/how-to-use-memo-use-callback
https://ddingg.tistory.com/119
useMemo vs. useEffect
https://stackoverflow.com/questions/56028913/usememo-vs-useeffect-usestate
useMemo와 useCallback 사용처
https://yceffort.kr/2022/04/best-practice-useCallback-useMemo
React Devtools Profile
https://www.joshwcomeau.com/react/why-react-re-renders/
useCallback 사용처와 주의사항
https://www.developerway.com/posts/how-to-write-performant-react-code
https://dmitripavlutin.com/react-usecallback/
useCallback, useMemo를 남용하면 안 되는 이유
https://kentcdodds.com/blog/usememo-and-usecallback
https://nicozerpa.com/when-to-use-usememo-and-usecallback-in-react/
https://amberwilson.co.uk/blog/how-and-when-to-use-react-usecallback/

 

ARP

1. ARP(Address Resolution Protocol)란?

  • IP 주소로부터 MAC 주소를 구하는 IP와 MAC 주소의 다리 역할을 하는 프로토콜
  • 주소 확인 프로토콜은 MAC 주소를 IP 주소에 매핑하는 데 사용되는 계층 2 프로토콜
    1. 호스트가 LAN(Local Area Network)에서 IP 주소 10.5.5.1과 같은 다른 호스트로 패킷을 보내려고 할 때 먼저 ARP 패킷을 보냄(브로드캐스트).
    2. ARP 패킷에는 간단한 질문이 포함되어 있음
    • IP 주소 10.5.5.1에 해당하는 MAC 주소는 무엇입니까?
    1. IP 주소를 사용하도록 구성된 호스트는 MAC 주소가 포함된 ARP 패킷으로 응답
  • RARP를 통해 실제 주소인 MAC 주소를 가상 주소인 IP 주소로 변환하기도 함
    • 이 매핑 절차는 IP 주소와 MAC 주소의 길이가 다르기 때문에 중요하며, 시스템이 서로를 인식할 수 있도록 변환이 필요함
    • MAC 주소는 데이터 링크 계층이라고도 하며, 물리적으로 연결된 두 장치 간의 연결을 설정하고 종료하여 데이터 전송이 이루어질 수 있도록 함
    • IP 주소는 네트워크 계층 또는 다른 라우터를 통해 데이터 패킷을 전달하는 역할을 하는 계층이라고도 함
    ⇒ ARP는 이러한 계층 사이에서 작동함

2. ARP 동작방식

동작과정

  1. 새 컴퓨터가 근거리 통신망(LAN)에 연결되면 식별 및 통신에 사용할 고유한 IP 주소를 받게 됨
  2. 데이터 패킷은 특정 호스트 시스템을 대상으로 하는 게이트웨이에 도착함
  3. 게이트웨이 또는 데이터가 한 네트워크에서 다른 네트워크로 흐르도록 하는 네트워크의 하드웨어는 ARP 프로그램에 IP 주소와 일치하는 MAC 주소를 찾도록 요청함
  4. 장치가 LAN에 연결된 다른 장치로 데이터를 보내기 위해 MAC 주소를 요청할 때마다 장치는 IP-MAC 주소 연결이 이미 완료되었는지 확인하기 위해 ARP 캐시를 확인함
  5. 캐시가 존재하는 경우 새 요청이 필요하지 않지만, 아직 변환이 수행되지 않은 경우 네트워크 주소에 대한 요청이 전송되고 ARP가 수행됨
    1. 사용자의 장치가 대상 호스트의 하드웨어 주소를 모르는 경우 장치는 이 주소를 요청하는 네트워크의 모든 호스트에 메시지를 보내고  적절한 대상 호스트가 요청을 알게 되면 하드웨어 주소로 응답한 다음 ARP 디렉토리 또는 테이블(ARP 캐시)에 저장됨

[예제]

PC1은 동일한 LAN 및 동일한 서브넷 10.1.1.0/24에 있는 PC3에 ping을 시도함

사용자가 ping 10.1.1.3 명령을 실행하면 PC1은 ICMP 요청(ping)을 네트워크를 통해 보내기 전에 이더넷 프레임에 캡슐화하기 시작함

  • PC가 프로토콜 데이터 단위(PDU)를 구성하는 방법
    • 계층 4에서
    PC1은 ICMP 에코 요청을 보내 ping이 작동한다는 것을 알고 ICMP 에코 응답을 기다림⇒ 따라서 이 계층에 필요한 모든 것을 사용할 수 있음
    • 계층 3
    PC1에서 대상 IP 주소를 알고 있으며 ping 10.1.1.3 명령에서 사용자가 명시적으로 언급하므로 대상 IP 필드에 넣음⇒ 따라서 이 계층에 필요한 모든 것을 사용할 수 있음
    • 계층 2
    PC1은 자신의 구성된 MAC 주소를 알고 소스 필드에 넣음따라서 목적지 MAC 주소는 PC1에서 사용할 수 없으며 ARP 를 사용해야 함
  • 그러나 PC1이 LAN의 어떤 최종 클라이언트에 10.1.1.3이 구성되어 있고 MAC 주소가 무엇인지 알 수 있는 방법은 없음
  • PC1은 자체적으로 구성된 IP 주소 10.1.1.1을 알고 이를 소스 필드에 넣음
  • 이에, 레이어 4의 프로토콜을 에코 요청 플래그가 설정된 ICMP로 설정

ARP 캐시?

  • ARP 캐시는 각 IP 주소 및 일치하는 MAC 주소 목록을 유지함
  • ARP 캐시는 동적이지만 네트워크의 사용자는 IP 주소와 MAC 주소가 포함 된 고정 ARP 테이블을 구성할 수도 있음
  • ARP 캐시는 IPv4 이더넷 네트워크의 모든 운영 체제에 보관됨
  • ARP 캐시 크기는 설계에 따라 제한되며 주소는 캐시에 몇 분 동안만 유지되는 경향이 있음
  • 공간을 확보하기 위해 정기적으로 제거됨
  • 이 디자인은 또한 사이버 공격자가 IP 주소를 도난당하거나 스푸핑하는 것을 방지하기 위한 개인 정보 보호 및 보안을 위한 것임
  • ARP 캐시(Windows 운영 체제에서)를 보려면 명령 프롬프트를 열고 명령 —' arp -a '(따옴표 제외)를 입력하면 되고 ARP 테이블은 아래와 같음

ARP 요청/응답 메시지

  • MAC 주소가 ARP 테이블에 없으면 소스 장치는 ARP 요청 메시지를 생성하고 요청 메시지에서 소스는 자신의 MAC 주소, IP 주소, 대상 IP 주소 및 대상 MAC 주소를 소스가 찾으려고 하므로 공백으로 남겨둠
  • ARP 응답 메시지에는 보내는 장치의 MAC 주소 가 포함되어 있고 대상 장치는 ARP 테이블을 업데이트하고 곧 소스에 연결해야 하므로 소스의 MAC 주소를 저장함 이제 소스가 이 장치의 대상(대상)이 되고 ARP 응답 메시지가 전송됨
  • 요청 메시지 예제
    • 대상 MAC 주소가 비어 있음(예 : 00:00:00:00:00:00 ).
    • 요청 메시지에는 다음과 같은 다양한 다른 필드가 포함됨
    1. 하드웨어 종류 - ARP 메시지를 전송할 때 사용하는 하드웨어의 종류를 지정함. 대부분의 하드웨어 유형은 이더넷임
    2. 프로토콜 유형 - IPv4 각 프로토콜에 번호가 할당되며 여기에서는 IPv4가 사용되었음
    3. 프로토콜 크기 - IPv4 주소의 길이(여기서는 4바이트).
    4. Opcode - ARP 메시지의 특성을 지정(ARP 요청용 1개 등)
    5. 소스 IP 주소 - 10.10.10.2
    6. 대상(대상) IP 주소 - 10.10.10.1
    7. 소스 MAC 주소 - 00:1a:6b:6c:0c:cc

  • ARP 응답 메시지 의 샘플 예입니다 .
    • 응답 메시지에는 소스에서 요청한 MAC 주소가 포함됨 (MAC 주소 00:1d:09:f0:92:ab는 ARP 응답 메시지에서 전송됨)

ARP 스푸핑?

  • ARP 스푸핑은 ARP 포이즌 라우팅 또는 ARP 캐시 포이즈닝이라고도 함
  • 이것은 사이버 범죄자가 네트워크 내의 합법적인 장치 또는 서버의 IP 주소와 MAC 주소를 연결할 의도로 가짜 ARP 메시지를 대상 LAN에 보내는 일종의 악의적인 공격임
  • 링크를 통해 피해자 컴퓨터의 데이터를 원래 대상 대신 공격자의 컴퓨터로 보낼 수 있음

⇒ ARP 스푸핑 공격은 피해자가 모르는 사이에 컴퓨터 간에 민감한 정보가 전달될 수 있으므로 위험할 수 있음

참고사항

  • MAC 주소는 고정되어 있지만 IP 주소는 지속적으로 업데이트됨
    • 사용자에게 보안과 개인정보보호를 제공하기 위해 지속적으로 변경됨
    • 제거 프로세스에서 사용되지 않는 주소는 삭제됨
    • 네트워크에 연결되지 않았거나 전원이 켜져 있지 않은 컴퓨터와의 통신 시도 실패와 관련된 모든 데이터도 마찬가지임

요약

  • ARP(Address Resolution Protocol)는 LAN: IP-to-MAC 바인딩 에서 주어진 논리적 주소( IP )의 물리적 주소( MAC)를 확인하는 메커니즘
  • ARP 요청 은 브로드캐스트 프레임일대일에 캡슐됨 ⇒ 따라서
  • 통신이며 LAN의 모든 호스트는 ARP 요청의 복사본을 받고 대상 IP의 소유자만 응답
  • ARP 응답은 유니캐스트 프레임 에 캡슐화됨⇒  따라서  요청자와 응답자 간의 일대일 통신임
  • 장치가 IP의 물리적 주소를 수신하면 **ARP 테이블(ARP 캐시)**에 항목을 생성하고 모든 후속 통신은 캐시된 항목을 사용함
  • ARP 테이블의 모든 항목은 기본적으로 일정 시간 동안 보관됩니다. 이것을 ARP Time Out이라고 함

3. 다양한 ARP

Proxy ARP

어떤 네트워크의 Proxy device가 해당 네트워크에 없는 IP 주소에 대한 ARP 요청에 응답하는 기술

즉, 네트워크 프록시가 네트워크 외부에 있는 IP 주소에 대한 ARP 쿼리에 응답할 수 있음

⇒ 이렇게 하면 패킷이 한 하위 네트워크에서 다른 하위 네트워크로 성공적으로 전송될 수 있음

Gratuitous ARP

네트워크 상의 호스트가 IP-to-MAC 주소를 안내하거나 갱신하는 방법을 수행하는 관리 절차와 같음

IP주소를 MAC주소로 변경하라는 ARP 요청을 통해 발생되지는 않음

Reverse ARP(RARP)

자신의 IP주소를 모르는 Host 기기가 이를 찾기위해 사용 가능

Inverse ARP(IARP)

ARP는 MAC주소를 찾기 위해 IP주소를 이용하지만, IARP는 IP주소를 찾기위해 MAC 주소를 활용함

라우팅 테이블

1. 라우팅 테이블?

  • IP 주소를 기반으로 라우터의 위치를 저장하는 테이블 또는 데이터베이스로서, 이 테이블은 다양한 네트워크에 대한 주소 맵 역할을 함
  • 송신지에서 수신지까지 도달하기 위해 사용되며, 라우터에 들어가 있는 목적지 정보들과 그 목적지로 가기 위한 방법이 들어있는 리스트
  • 라우팅 테이블에는 게이트웨이와 모든 목적지에 대해 해당 목적지에 도달하기 위해 거쳐야 할 다음 라우터의 정보를 가지고 있음
    • (참고) 게이트웨이 : 한 네트워크(segment)에서 다른 네트워크로 이동하기 위하여 거쳐야 하는 지점으로서, 서로 다른 네트워크 상의 통신 프로토콜을 변환해주는 역할을 하기도 함
  • 라우터는 패킷을 수신하면 대상 IP 주소를 검사하고 라우팅 테이블 을 조회하여 어떤 인터페이스 패킷을 보낼지 알아냄

2. 라우팅 테이블 요소

라우팅 테이블에 있는 각 항목의 기본 구성 요소

  • 목적지 주소: 목적지 네트워크의 IP 주소
  • 서브넷 마스크/넷마스크: 대상 주소의 클래스 또는 범위로, 대상 주소를 올바른 네트워크에 매핑하는 데 사용됨
  • 게이트웨이/다음 홉: 패킷이 전달되는 다음 IP 주소
  • 인터페이스: 목적지에 연결되는 outgoing 인터페이스
  • Metric: 패킷 전송을 위해 최적의 경로가 선택되도록 각 경로에 값을 할당하며, 경우에 따라 Metric은 대상 네트워크에 도달하기 위해 건너야 하는 홉 수 또는 라우터 수가 되기도 함. 경로가 여러 개 있는 경우 일반적으로 메트릭이 가장 낮은 경로가 선택됨

라우터가 저장할 수 있는 경로 유형

  • 직접 연결된 네트워크 ID
  • 원격 네트워크 ID
  • 호스트 경로
  • 기본 경로
  • 목적지

3. 라우팅 테이블 관리

직접 연결된 네트워크는 자동으로 추가됨

정적 라우팅 사용

정적 네트워크 장치에 대한 테이블은 네트워크 관리자가 수동으로 변경하지 않는 한 변경되지 않음

동적 라우팅 사용

동적 라우팅 에서 장치 는 라우팅 프로토콜을 사용하여 주변 네트워크 토폴로지에 대한 정보를 교환함으로써 라우팅 테이블을 자동으로 구축하고 유지함

동적 라우팅 테이블을 사용하면 장치가 네트워크를 "수신"하고 장치 오류 및 네트워크 혼잡과 같은 발생에 응답할 수 있음

4. 라우팅 테이블 작동방식

라우터 는 자신의 올바른 서브넷 (서브넷 ID, 보안을 유지하기 위해 더 큰 네트워크를 더 작은 네트워크로 나누는 것을 서브넷이라고 함)을 찾기 위해 데이터 패킷에 언급된 목적지 IP 주소와 모든 서브넷 마스크를 하나씩 Bitwise AND를 수행

  • 일치하는 항목이 하나만 있는 경우 라우터는 해당 인터페이스에 데이터 패킷을 전달
  • 둘 이상의 일치가 발생하면 라우터는 가장 긴 서브넷 마스크에 해당하는 인터페이스의 데이터 패킷을 전달
  • 일치하는 항목이 없으면 라우터는 기본 항목에 해당하는 인터페이스의 데이터 패킷을 전달

예제를 통해 알아보자

[1번째 항목]

PC1이 101.25.67.0에 있는 대상의 PC3에 패킷을 보내려고 함

그러나 PC3은 PC1의 네트워크에 없으므로 PC1은 이 패킷을 라우터 A로 전달

라우터 A에 도착하면 라우터는 대상 101.25.67.0에 대한 경로에 대한 라우팅 테이블을 확인

경로가 존재하므로 패킷은 라우터 A의 인터페이스 eth3을 통해 10.0.0.2의 게이트웨이로 전달

[2번째 항목]

PC1의 사용자가 네트워크 200.0.2.0의 PC25로 패킷을 보내려고 함

라우터 A는 라우팅 테이블에서 PC25가 있는 주소에 대한 항목을 확인

기록된 항목이 없기 때문에 라우터 A는 이 패킷을 eth0 인터페이스를 통해 10.0.0.1의 기본 게이트웨이에 연결된 다른 네트워크로 전달

기본 게이트웨이 경로는 항상 모든 라우팅 테이블에 있음

라우팅 테이블에 특정 네트워크에 대한 항목이 없을 때 사용함

기본 게이트웨이는 일반적으로 다른 원격 네트워크에 연결함

예를 들어 가정 환경에서 기본 게이트웨이는 인터넷에 연결됨

[3번째 항목]

첫 번째 항목과 마찬가지로 마지막 항목에서는 대상 192.25.67.0으로 주소가 지정된 패킷이 인터페이스 eth5를 통해 10.0.0.3의 게이트웨이로 전달됨

IP 주소체계

1. 클래스 기반(IPv4)

일반적으로 IP 주소를 쓰는 표기법에는 점으로 구분된 10진수 표기법과 16진수 표기법의 두 가지 표기법이 있음

[점+10진수 표기법 주의사항]

  1. 모든 세그먼트(바이트)의 값은 0에서 255(둘 다 포함) 사이임
  2. 어떤 세그먼트에서도 값 앞에 0은 없음(054 : X , 54 : O)

32비트 IP 주소는 5개의 하위 클래스로 나뉨 (클래스 A, 클래스 B, 클래스 C, 클래스 D, 클래스 E)

이러한 각 클래스에는 유효한 IP 주소 범위가 있음

클래스 D 및 E는 각각 멀티캐스트 및 실험 목적으로 예약되어 있음

첫 번째 옥텟의 비트 순서는 IP 주소의 클래스를 결정

IPv4 주소는 두 부분으로 나뉨

  • 네트워크 ID
  • 호스트 ID

IP 주소의 클래스는 네트워크 ID 및 호스트 ID에 사용되는 비트와 해당 특정 클래스에서 가능한 총 네트워크 및 호스트 수를 결정하는 데 사용

각 ISP 또는 네트워크 관리자는 네트워크에 연결된 각 장치에 IP 주소를 할당

[참고사항]

  • IP 주소는 IANA(Internet Assigned Numbers Authority) 및 RIR(지역 인터넷 레지스트리)에서 전 세계적으로 관리
  • 모든 네트워크의 첫 번째 IP 주소는 네트워크 번호이고 마지막 IP 주소는 브로드캐스트 IP용으로 예약되어 있으므로 호스트 IP 주소의 총 수 산출 시에, 2개의 IP 주소는 계산에서 제외

클래스 A

클래스 A에 속하는 IP 주소는 호스트가 많은 네트워크에 할당됩니다.

클래스 A에 속하는 IP 주소 범위는 1.xxx – 126.xxx입니다.

클래스 B

클래스 B에 속하는 IP 주소는 중대형 네트워크에 이르는 네트워크에 할당됩니다.

클래스 B에 속하는 IP 주소 범위는 128.0.xx – 191.255.xx입니다.

클래스 C

클래스 C에 속하는 IP 주소는 소규모 네트워크에 할당됩니다.

클래스 C에 속하는 IP 주소의 범위는 192.0.0.x – 223.255.255.x입니다.

클래스 D

클래스 D에 속하는 IP 주소는 멀티캐스팅을 위해 예약되어 있습니다.

클래스 D에는 서브넷 마스크가 없습니다. 클래스 D에 속하는 IP 주소의 범위는 224.0.0.0 – 239.255.255.255입니다.

클래스 E

클래스 E에 속하는 IP 주소는 실험 및 연구 목적으로 예약되어 있습니다. 클래스 E의 IP 주소 범위는 240.0.0.0 – 255.255.255.254입니다. 이 클래스에는 서브넷 마스크가 없습니다.

특수 IP 주소 범위

  • 169.254.0.0 – 169.254.0.16 : 링크 로컬 주소
  • 127.0.0.0 – 127.0.0.8 : 루프백 주소
  • 0.0.0.0 – 0.0.0.8 : 현재 네트워크 내에서 통신하는 데 사용

호스트 ID 할당 규칙

호스트 ID는 네트워크 내에서 호스트를 식별하는 데 사용됩니다. 호스트 ID는 다음 규칙에 따라 할당

  • 모든 네트워크 내에서 호스트 ID는 해당 네트워크에 대해 고유해야 함
  • 모든 비트가 0으로 설정된 호스트 ID는 이 호스트 ID가 IP 주소의 네트워크 ID를 나타내는 데 사용되기 때문에 할당할 수 없음
  • 모든 비트가 1로 설정된 호스트 ID는 이 호스트 ID가 해당 특정 네트워크에 있는 모든 호스트에 패킷을 보내기 위한 브로드캐스트 주소로 예약되어 있기 때문에 할당할 수 없음

네트워크 ID 할당 규칙

동일한 물리적 네트워크에 있는 호스트는 동일한 물리적 네트워크에 있는 모든 호스트에 동일한 네트워크 ID가 할당되므로 네트워크 ID로 식별됨. 네트워크 ID는 다음 규칙에 따라 할당됨.

  • 127은 클래스 A 주소에 속하고 내부 루프백 기능을 위해 예약되어 있기 때문에 네트워크 ID는 127로 시작할 수 없음
  • 1로 설정된 네트워크 ID의 모든 비트는 IP 브로드캐스트 주소로 사용하기 위해 예약되어 있으므로 사용할 수 없음
  • 0으로 설정된 네트워크 ID의 모든 비트는 로컬 네트워크의 특정 호스트를 나타내는 데 사용되며 라우팅되지 않으므로 사용되지 않음

Classful 주소 지정 요약

Classful 주소 지정 문제

이 클래스 주소 지정 방법의 문제는 수백만 개의 클래스 A 주소가 낭비되고 클래스 B 주소의 많은 부분이 낭비되는 반면 클래스 C에서 사용할 수 있는 주소의 수가 너무 적어 조직의 요구를 충족할 수 없다는 것임.

클래스 D 주소는 멀티캐스트 라우팅에 사용되므로 단일 블록으로만 사용할 수 있음.

클래스 E 주소는 예약되어 있음.

⇒ 이러한 문제가 있기 때문에 Classful 네트워킹은 1993년 CIDR(Classless Inter-Domain Routing)로 대체됨

2. CIDR(Classless Inter-Domain Routing)

                        (예) **192.168.10.70/26**

CIDR?

  • 네트워크 정보를 여러개로 나누어진 Sub-Network들을 모두 나타낼 수 있는 하나의 Network로 통합해서 보여주는 방법
  • 클래스가 없다는 뜻은 네트워크 구분을 아래와 같이 Class로 하지 않는다는 것
  • 도메인간의 라우팅에 사용되는 인터넷 주소를 원래 IP주소 클래스 체계를 쓰는 것보다 더욱 능동적으로 할당하여 지정하는 방식
  • Class 체계보다 더 유연하게 IP주소를 여러 네트워크 영역으로 나눌 수 있게 됨
  • 서브네팅 뿐만아니라 서브넷을 합치는 슈퍼네팅 역시 CIDR의 일환이며, 정리하자면, 서브네팅, 슈퍼네팅 이러한 IP나누고 합치는 기법이 모두 CIDR

  • Intra-Domain과 같이 각 네트워크 대역을 구분 짓고 Inter-Domain 과 같이 구분된 네트워크간 통신을 위한 주소 체계

CIDR를 계산해보자

[예제1] 143.7.65.203/24

CIDR이 "/24" 라면 아래 그림과 같이 앞에서부터 24비트 이후에 오는 4번째 옥텟(파란색 부분)을 전부 사용할 수 있다는 표현

하나의 옥텟은 8비트로 2의8승 인 256개 이기 때문에,  143.7.65.203/24 일때 143.7.65.0 ~ 143.7.65.255 까지 사용이 가능한 것

이렇게 CIDR값이 각 자리의 옥텟을 전체를 포함하는 /8, /16, /24, /32 일 경우는 계산하기 쉬움.

0부터 그자리에 해당하는 255 까지 라고 보면되기 때문

⇒ 결국 143.7.65.203/16 이라면 143.7.0.0 ~ 143.7.255.255 가 됨

[예제2] 143.7.65.203/23

사이더 값이 23일경우 아래와 같이 노란색과 파란색 두개의 옥텟에 걸치게 되므로 바로 계산하기가 애매해 짐 이 경우 애매하게 걸친 옥텟을 2진수로 표현하고 최소 값과 최대값을 찾아 내야 함

예를들어 143.7.65.203/23 일때, 4번째 옥텟(파란색 부분) 전체와 3번째 옥텟(노란색 부분)영역의 1비트가 포함됨

그럼 애매하게 걸친 3번째 옥텟을 2진수로 표현해보면,  65로 01000001 가 됨.

사이더에 의해 마지막 자리 1비트를 0 또는 1을 사용할 수 있게 되면 01000000, 01000001 이기 때문에 64, 65가 됨.

여기서 64가 3번째 옥텟에서 사용할 수 있는 최소값이 되며 최대값은 65가 됨.

나머지 4번째 옥텟(파란색 부분)은 전체를 사용할 수 있기 때문에 최소값 0, 최대값 255가 됨.

⇒ 따라서 143.7.65.203/23 는  143.7.64.0 ~ 143.7.65.255 대역을 사용할 수 있는 것

[예제3] 143.7.65.203/22

01000000, 01000001,  01000010, 01000011 총 4개를 사용할 수 가 있을 것이며, 이를 10진수로 표현하면 최소값인 64부터, 65, 66, 최대값인 67 가 됨.

즉  143.7.65.203/22 는  143.7.64.0 ~ 143.7.67.255 임.

[예제4] 143.7.65.203/25

4번째 옥텟(파란색 부분)을 2진수로 고쳐서 표현할 수 있는 최소값 최대값을 찾아 내야 함.

십진수 203은 2진수로 11001011 입니다. 여기서 최소 값은  10000000 인 128이고, 최대값은 11111111인 255 임

⇒ 따라서 143.7.65.128 ~ 143.7.65.255 가 됨.

[예제5] 192.168.10.70/26

  • 192.168.10.70/26 에서 "/26"이 사이더 표기법임(이 숫자는 비트 단위이며 0~32 까지 표현이 가능)
  • 192.168.10.70/26 풀이법

[참고 : 서브넷 마스크와 IP 주소 간에는 &연산이 일어남]

  • 서브넷마스크의 1의 갯수가 26개
  • 네번째 옥텟의 최상위 2개의 비트가 1이고, 나머지 6비트를 호스트ID로 할당
  • 그러면 서브넷마스크는 255.255.255.192
  • 256 - 192 = 64 (특수목적 IP - 2) = 62 이므로 62개의 호스트를 가질 수 있게 된다.
  • 256 / 64 = 4이므로 4개의 서브넷 네트워크를 가진다는것도 알수 있다.
  • 그래서 사용할 수 있는 IP의 대역에 대한 정보는 다음과 같이 된다.

  • 단, 각 네트워크의 첫번째와 마지막 IP는 사용이 불가능
  • 정리하자면, 192.168.10.70/26 은 이렇게 분리된 4개의 네트워크와 각 62개의 호스트를 가진 네트워크를 사용한다는 것을 의미하며, 자신이 두번째 네트워크에 속해 있다

장점

  • 단 한줄만으로 네트워크 범위를 추측 또는 측정가능
  • /24의 숫자를 필요에따라 더 쓰거나 적게 사용하여 IP를 적절하게 할당할 수 있음 : 서브넷팅(더 적게 쓰기) 와 슈퍼넷팅 (더 많이 쓰기)
    • 서브넷 마스크를 호스트ID 방향(오른쪽)으로 움직이면(값 증가) 서브네팅이고 네트워크 방향(왼쪽)으로 움직이면(값 감소) 슈퍼네팅임
    • 만일 서브네팅을 추가적으로 더 해야 되면 /24를 증가시켜 /25로 표기해 제공 해주면 되고, 반대로 슈퍼네팅을 해야된다면 /24를 감소시켜 /23으로 표기해 제공해주면 됨
  • 이진 배수를 통해 효율적으로 주소 공간 할당 가능
  • 클래스 불균형 제거를 통해, 주소 공간의 일부를 널리 사용할 수 있음

단점

  • 클래스를 기반으로 하는 이전 시스템의 주요 이점은 단순성이었음(첫 번째 옥텟을 보고 IP 주소의 몇 비트가 네트워크 ID를 나타내고 호스트 ID가 몇 비트인지 결정할 수 있었음)
  • CIDR의 주요 단점은 복잡성임

3. DHCP

DHCP(Dynamic Host Configuration Protocol)?

네트워크의 각 호스트가 효율적으로 통신할 수 있도록 IP 주소 및 기타 정보를 동적으로 할당하는 데 사용되는 네트워크 관리 프로토콜

DHCP는 네트워크 관리자의 작업을 용이하게 하는 IP 주소 할당을 자동화하고 중앙에서 관리

인터넷 프로토콜(IP) 호스트에 해당 IP 주소와 서브넷 마스크 및 기본 게이트웨이와 같은 기타 관련 구성 정보를 자동으로 제공하는 클라이언트/서버 프로토콜

DHCP는 IP 주소 외에도 서브넷 마스크, 기본 게이트웨이 및 DNS(Domain Name Server) 주소 및 기타 구성을 호스트에 할당하여 네트워크 관리자의 작업을 보다 쉽게 만들어 줌

DNS, NTP와 같은 네트워크 서비스 외에도, UDP 또는 TCP 기반의 모든 통신 프로토콜을 사용할 수 있음

DHCP 서버는 다른 IP 네트워크와 통신할 수 있도록 네트워크의 각 장치에 IP 주소 및 기타 네트워크 구성 매개변수를 동적으로 할당함

DHCP는 BOOTP라고 하는 이전 프로토콜의 향상된 기능임

DHCP 왜 쓰는데?

  1. 운영 작업 감소
  • 네트워크 관리자는 더 이상 네트워크를 사용하기 전에 각 클라이언트를 수동으로 구성할 필요가 없음
  • 중복되거나 잘못된 IP 할당이 없으므로 IP 주소 충돌이 없음
  • TCP/IP 기반 네트워크의 모든 장치에는 네트워크와 해당 리소스에 액세스할 수 있는 고유한 유니캐스트 IP 주소가 있어야 함
  • DHCP가 없으면 한 서브넷에서 다른 서브넷으로 이동하는 새 컴퓨터 또는 컴퓨터의 IP 주소를 수동으로 구성해야 함
  • 네트워크에서 제거된 컴퓨터의 IP 주소는 수동으로 회수해야 함 DHCP를 사용하면 이 전체 프로세스가 중앙에서 자동화되고 관리됨
  1. IP 주소 지정 계획이 최적화됨.
  • 더 이상 사용되지 않는 주소가 해제되고 연결하는 새 클라이언트에서 사용 가능
  • IP 주소는 고정(영구 할당)이 아닌 동적(임대)이므로 더 이상 사용하지 않는 주소는 재할당을 위해 자동으로 풀로 반환됨
  1. 사용자 이동성을 쉽게 관리할 수 있음
  • 관리자는 네트워크 액세스 포인트가 변경될 때 클라이언트를 수동으로 재구성할 필요가 없음
  • 무선 네트워크의 다른 위치로 이동하는 휴대용 장치와 같이 자주
  • 업데이트해야 하는 클라이언트의 IP 주소 변경을 효율적으로 처리

DHCP의 구성 요소

  1. DHCP 서버
  2. 일반적으로 네트워크 구성 정보를 보유하는 서버 또는 라우터
  3. DHCP 클라이언트
  4. 다른 컴퓨터나 모바일과 마찬가지로 서버에서 구성 정보를 가져오는 끝점
  5. DHCP 릴레이 에이전트(DHCP 패킷이 라우터를 통해 이동할 수 없기 때문에, DHCP 서버가 모든 네트워크의 요청을 처리할 수 있도록 릴레이 에이전트가 필요)
  6. 여러 LAN에 대해 하나의 DHCP 서버만 있는 경우 모든 네트워크에 있는 DHCP 릴레이 에이전트가 DHCP 요청을 서버로 전달
  7. IP 주소 풀
  8. 클라이언트에 할당할 수 있는 IP 주소 목록을 포함
  9. 서브넷 마스크
  10. 현재 존재하는 네트워크를 호스트에게 알려줌
  11. 임대 시간
  12. 클라이언트가 IP 주소를 사용할 수 있는 시간임.  이 시간이 지나면 클라이언트는 IP 주소를 갱신해야 함.
  13. 게이트웨이 주소
  14. 게이트웨이 주소는 호스트가 게이트웨이가 인터넷에 연결할 위치를 알려줌.

DHCP는 어떻게 동작할까?

  • 기본 흐름
    • (요약) 응용 프로그램 계층에서 작동하여 IP 주소를 클라이언트에 동적으로 할당하며 이는 DHCP 트랜잭션 또는 DHCP 대화라고 하는 일련의 메시지 교환을 통해 발생
    • DHCP 서버가 관리자의 정책에 따라 구성 데이터를 요청하는 클라이언트에 전달
    • 요청된 공통 네트워크 매개변수( " DHCP 옵션 " )에는 서브넷 마스크, 라우터, 도메인 이름 서버, 호스트 이름 및 도메인 이름이 포함됨
    • 요청하는 클라이언트는 네트워크에 가입할 때 IP 주소가 없으므로 요청을 브로드캐스트함
    ⇒ 따라서 프로토콜은 IP 통신의 초기 단계에서 사용됨
    • 이러한 동적 프로토콜이 IP 주소를 가져오는 데 사용되지 않는 경우 클라이언트는 일반적으로 "고정 IP 주소"라고 하는 미리 정의된 IP 주소를 사용해야 함.
    • 이 주소는 구성 파일 또는 특정 명령으로 클라이언트 네트워크 인터페이스에서 수동으로 구성됩니다.

DHCP 동작 예제

  • DHCP 검색 DHCP 클라이언트는 DHCP 서버를 검색하기 위해 메시지를 브로드캐스트함 클라이언트 컴퓨터는 기본 브로드캐스트 목적지(255.255.255.255) 또는 설정된 특수 서브넷 브로드캐스트 주소와 함께 패킷을 보냄
    • 255.255.255.255 : “현재 네트워크” 로서, 이 주소를 사용하면 연결된 네트워크에 브로드캐스트 패킷을 보낼 수 있음.

  • DHCP Offer:
  • DHCP 서버가 DHCP Discover 메시지를 수신하면 클라이언트에게 DHCP 제안 메시지를 전송하여 클라이언트에게 IP 주소(IP 주소 풀 형식)를 제안함 이 DHCP 제안 메시지에는 DHCP 클라이언트에 대해 제안된 IP 주소, 서버의 IP 주소, 클라이언트의 MAC 주소, 서브넷 마스크, 기본 게이트웨이, DNS 주소 및 임대 정보가 포함됨

  1. DHCP 클라이언트에 대해 제안된 IP 주소 : 192.168.1.11
  2. 네트워크를 식별하기 위한 서브넷 마스크 : 255.255.255.0
  3. 서브넷의 기본 게이트웨이 IP : 192.168.1.1
  4. 이름 번역을 위한 DNS 서버의 IP : 8.8.8.8
  • DHCP 요청그러나 클라이언트는 하나의 DHCP 제안만 수락나머지 DHCP 서버에서 제공되는 다른 모든 IP 주소는 철회되고 사용 가능한 IP 주소 풀로 반환됨
  • 제안에 대한 응답으로 클라이언트는 DHCP 서버 중 하나에서 제안된 주소를 요청하는 DHCP 요청을 보냄
  • 대부분의 경우 클라이언트는 네트워크에 많은 DHCP 서버가 있기 때문에(결함에 대한 용인을 제공하므로), 한 서버의 IP 주소 지정이 실패하면 다른 서버가 백업을 제공할 수 있음

  • DHCP Acknowledgement서버는 클라이언트가 요청할 수 있는 다른 설정을 보낼 수도 있음
  • 이 단계에서 IP 구성이 완료되고 클라이언트는 새 IP 설정을 사용할 수 있음
  • 그런 다음 서버는 클라이언트에 대한 DHCP 임대를 확인하는 Acknowledgement를 클라이언트에 보냄

DHCP는 장점만 있나? 단점은 뭐지?

  1. 보안 위험성 DHCP 서버에는 클라이언트 인증을 위한 보안 메커니즘이 없으므로 모든 새 클라이언트가 네트워크에 참여할 수 있음 ⇒ 이는 승인되지 않은 클라이언트에 IP 주소가 부여되고 승인되지 않은 클라이언트로부터 IP 주소가 고갈되는 것과 같은 보안 위험이 있음
  2. 실패의 Single Point가 될 수 있음 네트워크에 DHCP 서버가 하나만 있는 경우 DHCP 서버는 실패의 Single Point가 될 수 있음

4. NAT(Network Address Translation)

  • NAT는 네트워크 주소 변환 을 나타냄
  • NAT의 개념은 여러 장치가 단일 공용 주소를 통해 인터넷에 액세스할 수 있도록 하는 것임
  • 이를 위해서는 사설 IP 주소를 공인 IP 주소로 변환해야 함 ⇒ 주어진 개인 IP 주소 세트를 게이트웨이 장치에 연결된 단일 공용 IP 주소로 변환함
  • 로컬 호스트에 인터넷 액세스를 제공하기 위해 하나 이상의 로컬 IP 주소를 하나 이상의 글로벌 IP 주소로 또는 그 반대로 변환하는 프로세스임즉, 대상으로 라우팅될 패킷에서 호스트의 포트 번호를 다른 포트 번호로 마스킹하고 NAT 테이블에 해당하는 IP 주소 및 포트 번호 항목을 만듦.
  • 또한 포트 번호 변환을 수행함
  • NAT는 일반적으로 라우터 또는 방화벽에서 작동
  • 일반적으로 경계 라우터가 NAT용으로 구성됨패킷이 로컬(내부) 네트워크 외부를 통과하면 NAT는 해당 로컬(개인) IP 주소를 글로벌(공용) IP 주소로 변환하고 패킷이 로컬 네트워크에 들어오면 글로벌(공용) IP 주소가 로컬(사설) IP 주소로 변환됨
  • NAT에 주소가 부족한 경우, 즉 구성된 풀에 주소가 남아 있지 않으면 패킷이 삭제되고 ICMP(Internet Control Message Protocol) 호스트에 연결할 수 없는 패킷이 대상으로 전송됨
  • 즉, 로컬(내부) 네트워크에 하나의 인터페이스가 있고 글로벌(외부) 네트워크에 하나의 인터페이스가 있는 라우터.
  • 예를 들어, 조직의 홈 모뎀 또는 방화벽 장치임그러나 문제는 사설 IP 주소를 가진 장치가 있는 사설 네트워크가 있을 때 대상 서버가 사설 IP 범위로 응답을 라우팅할 수 없기 때문에 트래픽을 공용 인터넷으로 라우팅할 수 없다는 것또한, 사설 네트워크를 외부 네트워크로부터 안전하게 보호할 수 있음
  • ⇒ 여기에서 NAT이 이용됨.  공용 주소로 ISP에 연결된 가정이나 조직의 게이트웨이 장치가 Nating을 수행함. AWS, Azure 또는 Google 클라우드와 같은 클라우드 플랫폼의 경우 NAT 게이트웨이는 프라이빗 서브넷의 서버가 외부 세계와 통신할 수 있도록 퍼블릭 서브넷에 배포됨
  • IPV4 고갈 문제를 해결하기 위해 사설 IP 범위가 도입되었으며 조직은 수만 대의 컴퓨터와 서버에 대해 사설 IP 범위를 사용할 수 있었음

NAT는 어떻게 작동할까?

예제를 통해 알아보자

다음은 NAT 장치(라우터)를 통해 전달되는 요청의 상위 수준 아키텍처임

1단계 사설 네트워크에 있는 컴퓨터가 공용 웹사이트에 액세스를 시도함.  이 예에서는 google.com으로 가정

2단계 요청 패킷이 먼저 라우터에 도달함.  라우터에는 public IP 주소가 있음.  아래 이미지와 같이 public address를 요청 IP 패킷 헤더에 소스 IP 주소로 추가하고 요청을 전달함.  (이 과정을 "masquerading"이라 부르기도 함)

3단계 요청이 서버(google.com)에 도달하고 소스를 서버 IP로 변경하고 대상을 라우터 공용 IP로 변경하여 웹페이지를 응답으로 보냄

4단계

응답이 라우터에 도달하면 대상 주소를 요청된 컴퓨터의 사설 IP 주소로 수정

라우터는 **NAT 테이블(**NAT 지원 장치에서 관리하는 고유한 테이블)을 통해 요청을 추적함

라우터는 NAT 테이블 정보를 사용하여 요청이 시작된 장치와 응답을 다시 보내야 하는 장치를 알고 있음

NAT은 뭐가 좋지?

NAT는 IPV4 고갈과 관련된 문제를 해결

RFC1918 (사설 IP 범위)은 NAT로 인해 가능

RFC1918 서브넷의 범위는 다음과 같음.

  1. 10.0.0.0/8
  2. 172.16.0.0/16(172.16/12 접두사)
  3. 192.168.0.0/16

NAT FAQ

  • NAT는 MAC 주소를 매핑합니까?
  • 아니오. NAT는 패킷을 처리하는 네트워크 계층(계층 3)에서 작동합니다. MAC 주소는 데이터 링크 계층에 속합니다.
  • 공용 IP 및 인터넷 연결이 있는 서버에 NAT가 필요합니까?
  • 아니오. NAT는 사설 IP 범위가 있는 장치가 인터넷에 연결할 수 있도록 설계되었습니다.
  • 포트 번호를 마스킹하는 이유는 무엇입니까?
  • 네트워크에서 두 호스트 A와 B가 연결되어 있다고 가정합니다. 이제 둘 다 호스트 측에서 동일한 포트 번호(예: 1000)에서 동일한 대상을 동시에 요청합니다. NAT가 IP 주소만 변환하는 경우 패킷이 NAT에 도착하면 두 IP 주소가 모두 네트워크의 공용 IP 주소로 마스킹되어 대상으로 전송됩니다. 대상은 라우터의 공용 IP 주소로 응답을 보냅니다. 따라서 응답을 수신하면 어떤 응답이 어느 호스트에 속하는지 NAT에 명확하지 않습니다(A와 B의 소스 포트 번호가 동일하기 때문). 따라서 이러한 문제를 피하기 위해 NAT는 소스 포트 번호도 마스킹하고 NAT 테이블에 항목을 만듭니다.

NAT Types(3종류)

  1. Static NAT

로컬주소가 공용 주소로 변환될 때, NAT은 같은 것을 선택함.

이것은, NAT 장치나 라우터에 연관된 일관성있는 공용 IP가 있다는 것을 뜻함.

  1. Dynamic NAT

매번 같은 IP 주소를 선택하는 것 대신에, 이 NAT은 공용 주소 풀을 지남.

이것은 라우터가 로컬 주소를 공용 주소로 변환할 때마다 라우터나 NAT 장치가 다른 주소를 얻는 결과를 낳음.

  1. PAT

PAT은 port 주소 변환을 위해 있음.

dynamic NAT의 종류이지만, 몇 가지 로컬 IP 주소들을 하나의 공용 주소로 묶음

모든 직원들의 활동이 하나의 IP를 사용하길 원하는 조직들은 때때로 네트워크 관리의 감시 아래 PAT을 사용함

 

IPv4 vs. IPv6

  • 오늘날 시스템에 널리 구현되는 IP 프로토콜에는 IPv4와 IPv6의 두 가지 버전이 있음
  • Pv6은 프로토콜의 개선과 IPv4 주소 공간의 제한으로 인해 천천히 IPv4를 대체하고 있음  ⇒ 간단히 말해서, 현재 세계에는 IPv4를 통해 사용할 수 있는 주소의 양에 비해 너무 많은 인터넷 연결 장치가 있음
  • IPv4 주소는 32비트 주소입니다. 주소의 각 바이트 또는 8비트 세그먼트는 마침표로 나뉘며 일반적으로 숫자 0-255로 표시됨
  • 이러한 숫자는 일반적으로 인간의 이해를 돕기 위해 10진수로 표시되지만, 각 세그먼트는 일반적으로 8비트의 표현이라는 사실을 표현하기 위해 옥텟 (octet)이라고 함

IPv4 형태?

  • 일반적인 IPv4 주소는 다음과 같음
192.168.0.5
  • 각 옥텟에서 가장 낮은 값은 0이고 가장 높은 값은 255임
  • 이것을 이진법으로 표현하여 4개의 옥텟이 어떻게 보일지 더 잘 알 수 있음. 가독성을 위해 각 4비트를 공백으로 구분하고 점을 대시로 대체함.
1100 0000 - 1010 1000 - 0000 0000 - 0000 0101

⇒ 이 두 형식이 동일한 숫자를 나타낸다는 것을 인식하는 것은 나중에 개념을 이해하는 데 중요

IPv6 형태?

  • IPv4 및 IPv6의 프로토콜 및 백그라운드 기능에는 몇 가지 다른 차이점이 있지만 가장 눈에 띄는 차이점은 주소 공간임.
  • IPv6은 주소를 128비트 숫자로 표현함.⇒ 이 확장된 주소 범위를 표현하기 위해 IPv6은 일반적으로 4자리 16진수의 8개 세그먼트로 작성됨
  • 16진수는 숫자 0-9를 사용하여 숫자 0-15를 나타내고 더 높은 값을 나타내는 숫자 a-f를 사용함
  • ⇒ 이를 고려하면 IPv6에는 IPv4 주소 양의 28 배에 해당하는 7.9×10 이상의 공간이 있음.
  • 일반적인 IPv6 주소는 다음과 같음
1203:8fe0:fe80:b897:8990:8a7c:99bf:323d
  • 압축 형식으로 작성된 이러한 주소를 볼 수도 있음
  • IPv6 규칙을 사용하면 각 옥텟에서 선행 0을 제거하고 0으로 채워진 단일 그룹 범위를 이중 콜론(::)으로 바꿀 수 있음

IPv4의 특징

  • 비연결 프로토콜
  • 다양한 장치에 간단한 가상 통신 계층 생성 허용
  • 메모리가 덜 필요하고 주소를 기억하기 쉬움
  • 수백만 개의 장치에서 이미 지원되는 프로토콜
  • 비디오 라이브러리 및 회의 제공

IPv6의 특징

  • 계층적 주소 지정 및 라우팅 인프라
  • 상태 저장 및 상태 비저장 구성
  • 서비스 품질(QoS) 지원
  • 인접 노드 상호 작용을 위한 이상적인 프로토콜

IPv4와 IPv6의 주요 차이점

  • IPv4는 32비트 IP 주소이고 IPv6은 128비트 IP 주소
  • IPv4는 숫자 주소 지정 방법인 반면 IPv6은 영숫자 주소 지정 방법
  • IPv4 바이너리 비트는 점(.)으로 구분되는 반면 IPv6 바이너리 비트는 콜론(:)으로 구분
  • IPv4는 12개의 헤더 필드를 제공하는 반면 IPv6은 8개의 헤더 필드를 제공
  • IPv4는 브로드캐스트를 지원하지만 IPv6은 브로드캐스트를 지원하지 않음
  • IPv4에는 체크섬 필드가 있지만 IPv6에는 체크섬 필드가 없음
  • IPv4와 IPv6을 비교할 때 IPv4는 VLSM(Variable Length Subnet Mask)을 지원하지만 IPv6은 VLSM을 지원하지 않음
  • IPv4는 ARP(Address Resolution Protocol)를 사용하여 MAC 주소에 매핑하는 반면 IPv6은 NDP(Neighbour Discovery Protocol)를 사용하여 MAC 주소에 매핑

IPv4 주소와 IPv6 주소의 차이점 요약

차이점의 근거  IPv4 IPv6
IP 주소의 크기 IPv4는 32비트 IP 주소입니다. IPv6은 128비트 IP 주소입니다.
주소 지정 방법 IPv4는 숫자 주소이며 이진 비트는 점(.) IPv6은 이진 비트가 콜론(:)으로 구분되는 영숫자 주소입니다. 16진수도 포함합니다.
헤더 필드 수 12 8
헤더 파일의 길이 20 40
체크섬 체크섬 필드가 있음 체크섬 필드가 없습니다.
예시 12.244.233.165 2001:0db8:0000:0000:0000:ff00:0042:7879
주소 유형 유니캐스트, 브로드캐스트 및 멀티캐스트. 유니캐스트, 멀티캐스트 및 애니캐스트.
Class 수 IPv4는 5가지 클래스의 IP 주소를 제공 합니다. 클래스 A에서 E. lPv6은 무제한의 IP 주소를 저장할 수 있습니다.
구성 새로 설치된 시스템이 다른 시스템과 통신하려면 먼저 구성해야 합니다. IPv6에서 구성은 필요한 기능에 따라 선택 사항입니다.
VLSM 지원 IPv4는 VLSM(가변 길이 서브넷 마스크)을 지원합니다. IPv6은 VLSM을 지원하지 않습니다.
분열 단편화는 경로를 보내고 전달하여 수행됩니다. 조각화는 발신자가 수행합니다.
라우팅 정보 프로토콜(RIP) RIP는 routed 데몬에서 지원하는 라우팅 프로토콜입니다. RIP는 IPv6을 지원하지 않습니다. 정적 경로를 사용합니다.
네트워크 구성 네트워크는 수동으로 구성하거나 DHCP를 사용하여 구성해야 합니다. IPv4에는 더 많은 유지 관리 노력이 필요한 인터넷 성장을 처리하기 위한 여러 오버레이가 있습니다. IPv6은 자동 구성 기능을 지원합니다.
최고의 기능 단일 NAT 주소를 허용하는 NAT(네트워크 주소 변환) 장치를 광범위하게 사용하면라우팅할 수 없는 수천 개의 주소를 마스킹하여 종단 간무결성을 달성할 수 있습니다. 방대한 주소 공간 으로 인해 직접 주소 지정이 가능 합니다.
주소 마스크 호스트 부분에서 지정된 네트워크에 사용합니다. 사용되지 않습니다.
SNMP SNMP는 시스템 관리에 사용되는 프로토콜입니다. SNMP는 IPv6을 지원하지 않습니다.
이동성 및 상호 운용성 이동이 제한되는 상대적으로 제한된 네트워크 토폴로지는 이동성과 상호 운용성 기능을 제한합니다. IPv6은네트워크 장치에 내장된 상호 운용성 및 이동성 기능을 제공합니다.
보안 보안은 애플리케이션에 따라 다릅니다. IPv4는 보안을 염두에 두고 설계되지 않았습니다. IPSec(Internet Protocol Security)은 IPv6 프로토콜에 내장되어적절한 키 인프라와 함께 사용할 수 있습니다.
패킷 크기 패킷 크기 576바이트 필요, 조각화 선택 사항 조각화 없이 1208바이트 필요
패킷 단편화 라우터 및 전송 호스트에서 허용 호스트만 보내기
패킷 헤더 체크섬 옵션을 포함하는 QoS 처리를 위한 패킷 흐름을 식별하지 않습니다. 패킷 헤드에는 QoS 처리를 위한 패킷 흐름을 지정하는 흐름 레이블 필드가 포함됩니다.
DNS 레코드 주소(A) 레코드, 호스트 이름 매핑 주소(AAAA) 레코드, 호스트 이름 매핑
주소 구성 수동 또는 DHCP를 통해 ICMPv6(Internet Control Message Protocol 버전 ​​6) 또는 DHCPv6을 사용한 상태 비저장 주소 자동 구성
IP 대 MAC 해상도 브로드캐스트 ARP 멀티캐스트 이웃 요청
로컬 서브넷 그룹 관리 인터넷 그룹 관리 프로토콜 GMP) 멀티캐스트 수신기 검색(MLD)
선택 필드 선택적 필드가 있음 선택적 필드가 없습니다. 그러나 확장 헤더를 사용할 수 있습니다.
IPSec 네트워크 보안과 관련된 IPSec(인터넷 프로토콜 보안)은 선택 사항입니다. IPSec(Internet Protocol Security) 네트워크 보안 관련 필수
동적 호스트 구성 서버 클라이언트는 네트워크에 연결하려고 할 때마다 DHCS(동적 호스트 구성 서버)에 접근합니다. 클라이언트는 영구 주소가 제공되므로 이러한 서버에 접근할 필요가 없습니다.
매핑 ARP(Address Resolution Protocol)를 사용하여 MAC 주소에 매핑 NDP(Neighbour Discovery Protocol)를 사용하여 MAC 주소에 매핑
모바일 장치와의 호환성 IPv4 주소는 점-10진수 표기법을 사용합니다. 그렇기 때문에 모바일 네트워크에는 적합하지 않습니다. IPv6 주소는 콜론으로 구분된 16진수로 표시됩니다. IPv6은 모바일 네트워크에 더 적합합니다 .

듀얼 IP?

  • IPv4와 IPv6은 서로 통신할 수 없지만 동일한 네트워크에 함께 존재할 수 있고 이것을 듀얼 스택이라 함
  • Dual-IP 스택을 사용하면 컴퓨터, 라우터, 스위치 및 기타 장치가 두 프로토콜을 모두 실행하지만 IPv6이 기본 프로토콜임

IPv6는 어떤 점이 좋을까?

  • 더 이상 NAT(네트워크 주소 변환)는 필요하지 않음
  • 자동 구성
  • 더 이상 개인 주소 충돌이 없음
  • 더 나은 멀티캐스트 라우팅
  • 더 간단한 헤더 형식
  • 간소화되고 더 효율적인 라우팅
  • "흐름 레이블 지정"이라고도 하는 진정한 서비스 품질(QoS)
  • 내장된 인증 및 개인 정보 보호 지원
  • 유연한 옵션 및 확장
  • 더 쉬운 관리(더 이상 DHCP 필요 없음)
  • 더 많은 주소를 지원
    • IPv4는 인터넷 주소로 32비트 주소를 사용.⇒ 많은 것처럼 보일 수 있지만 현재 42억 9천만 개의 IP 주소가 모두 할당되어 오늘날 우리가 직면한 주소 부족 문제로 이어짐⇒ IPv6 주소의 수는 IPv4 주소의 수보다 1028배 더 많기 때문에, 인터넷 장치가 매우 오랫동안 확장될 수 있도록 충분한 IPv6 주소가 있음.
    • ⇒ IPv6은 128비트 인터넷 주소를 사용하기 때문에, 2^128개의 인터넷 주소(정확히 340,282,366,920,938,463,463,374,607,431,768,211,456)를 지원 가능
    • ⇒ 즉, 총 2^32개의 IP 주소(약 42억 9천만 개)를 지원

 

 

부탁드리는 사항

혹시 잘못된 내용이나, 인용/차용 등에 있어 문제의 소지가 되는 내용이 있다면 언제든 알려주시면 큰 도움이 될 것 같습니다!

긴 글 읽어주셔서 감사합니다 :)

 

 

 

출처
정의들 (책) 면접을 위한 CS 전공지식 노트
ARP란? & ARP 작동방식
https://www.fortinet.com/resources/cyberglossary/what-is-arp https://www.techtarget.com/searchnetworking/definition/Address-Resolution-Protocol-ARP https://www.ibm.com/docs/en/zos-basic-skills?topic=layer-address-resolution-protocol-arp https://www.networkacademy.io/ccna/ethernet/what-is-arp
https://www.geeksforgeeks.org/how-address-resolution-protocol-arp-works/ https://www.juniper.net/documentation/en_US/junose15.1/topics/concept/ip-arp-understanding.html https://afteracademy.com/blog/what-is-arp-and-how-does-it-work
라우팅테이블 요소
https://www.geeksforgeeks.org/routing-tables-in-computer-network/
https://ipwithease.com/ip-routing-table-and-its-components/
https://www.baeldung.com/cs/routing-table-entry
라우팅테이블 작동방식
https://www.grandmetric.com/2018/01/20/how-does-routing-table-work/
https://www.routerfreak.com/what-is-ip-routing-how-does-it-work/
라우팅테이블과 홉바이홉
https://www.techopedia.com/definition/2447/next-hop
라우팅테이블과 게이트웨이
https://docs.freebsd.org/doc/6.0-RELEASE/usr/share/doc/handbook/network-routing.html
IP주소체계-클래스기반
https://www.geeksforgeeks.org/introduction-of-classful-ip-addressing/
https://docs.oracle.com/cd/E19504-01/802-5753/planning3-78185/index.html https://www.meridianoutpost.com/resources/articles/IP-classes.php
https://study-ccna.com/classes-of-ip-addresses/ https://www.tutorialspoint.com/ipv4/ipv4_address_classes.htm
클래스기반 IP주소의 단점/한계
https://service.snom.com/display/wiki/Limitations+of+class-based+IP+addressing https://public.support.unisys.com/framework/publicterms.aspx?returnurl=%2Faseries%2Fdocs%2FClearPath-MCP-20.0%2F37877693-226%2Fsection-000021665.html
CIDER
https://www.digitalocean.com/community/tutorials/understanding-ip-addresses-subnets-and-cidr-notation-for-networking
https://kim-dragon.tistory.com/9 https://inpa.tistory.com/entry/WEB-🌐-CIDR-이-무얼-말하는거야-⇛-개념-정리-계산법
https://www.geeksforgeeks.org/cidr-full-form/
DHCP
https://www.efficientip.com/what-is-dhcp-and-why-is-it-important/
https://learn.microsoft.com/en-us/windows-server/networking/technologies/dhcp/dhcp-top https://afteracademy.com/blog/what-is-dhcp-and-how-does-it-work
NAT
https://www.comptia.org/content/guides/what-is-network-address-translation https://www.geeksforgeeks.org/network-address-translation-nat/
https://devopscube.com/what-is-nat-how-does-nat-work/
IP주소와 위치추적
https://www.iplocation.net/
IPv4의 단점
https://www.hitechwhizz.com/2020/08/6-advantages-and-disadvantages-drawbacks-benefits-of-ipv4.html
IPv4 vs. IPv6
https://www.geeksforgeeks.org/differences-between-ipv4-and-ipv6/ https://www.thousandeyes.com/learning/techtorials/ipv4-vs-ipv6 https://www.guru99.com/difference-ipv4-vs-ipv6.html

 

'Network' 카테고리의 다른 글

Network기기-스위치와 라우터(L2, L3 중심)  (6) 2022.11.05

Letter Combinations of a Phone Number - LeetCode

문제요약

2-9의 숫자로 이루어진 문자열이 주어지면, 해당 숫자들로 표현가능한 가능한 문자 조합을 모두 반환

입력

0 <= digits.length <= 4

digits[i] is a digit in the range ['2', '9'].

출력

return string[]

(in any order)

어떻게 풀지?

  1. 데이터 전처리 : dict로 각 숫자별 사용가능한 char 정리
  2. 파이썬의 product 라이브러리를 사용하자
  3. 이 때, map문법과 파이썬의 스프레드 문법(*)을 사용

느낀 점

이번 문제는 문법과 라이브러리만 잘 알면 바로 풀 수 있는 문제였다.

특히, 이번 문제에서 product안에 숫자별 가능한 알파벳 리스트를 담아줄 때, JavaScript의 map과 스프레드 문법이 생각났다.

파이썬에서 map 문법이 있는 건 알았지만, 스프레드 문법은 그간 쓸 일이 없어서 써본 적이 없었고 있는지도 몰랐는데 역시나 있었다.

하나의 언어에 기본적으로 있는 기능은 다른 언어에도 기본적으로 있을 확률이 높은 것 같다.(특히 나중에 나왔을수록…파이썬은 아마 거의 다 있을듯)

다른 언어도 알면 이런 점이 좋구나 생각했다.

 

- python map

https://www.geeksforgeeks.org/python-map-function/

- python spread operator

https://how.wtf/spread-operator-in-python.html

내 코드

from itertools import product

digits = '23'

def solution(digits):
    if digits == '':
        return []
    dict = {}
    dict['2'] = ['a', 'b', 'c']
    dict['3'] = ['d', 'e', 'f']
    dict['4'] = ['g', 'h', 'i']
    dict['5'] = ['j', 'k', 'l']
    dict['6'] = ['m', 'n', 'o']
    dict['7'] = ['p', 'q', 'r', 's']
    dict['8'] = ['t', 'u', 'v']
    dict['9'] = ['w', 'x', 'y', 'z']
    def find_strs(int_str):
        return dict[int_str]
    print(list(digits))
    print(list(map(find_strs, list(digits))))
    res = list(product(*list(map(find_strs, list(digits))), repeat=1))
    answer = [''.join(x) for x in res]
    return answer

print(solution(digits))

Valid Triangle Number - LeetCode

문제요약

정수배열이 주어질 때, 삼각형을 만들 수 있는 경우의 수를 구하시오.

입력

1 <= nums.length <= 1000

0 <= nums[i] <= 1000

출력

return int

어떻게 풀까?

1. 삼각형을 만들 수 있는 조건?

삼각형의 세 변의 길이 A, B, C 중에서 한 변의 길이를 뽑았을 때, 나머지 두 변의 길이의 합 보다 작아야 한다.(세 변의 길이 모두 부합하여야 함)

⇒ 이 규칙을 좀 더 간단히 해보면?

⇒ “가장 큰 변의 길이 < 나머지 변의 길이의 합”이 부합한다면, 나머지 2조건은 자동적으로 성립할 것

2. 가장 간단하게 푸는 방법?

정수 배열로 가능한 모든 순열 조합을 뽑아 낸 후에 삼각형 만들 수 있는 조건 검토

⇒ O(3N^3)으로 불가능

3. 시간복잡도는 얼마나 수용 가능한가?

⇒ O(10N^2) 정도까지 수용 가능할 것

4. 좀 더 단순화 해본다면?

제약조건을 걸어서 선택지를 좁혀보자

⇒ 가장 큰 숫자를 고정시키고 해당 숫자를 바탕으로 가능한 나머지 숫자 2개의 조합을 찾기

⇒ for largest_idx in range(len(nums)) : O(N)

⇒ 나머지 숫자를 찾는 방식을 O(N)으로 끝내야 함

⇒ 한 배열을 1번만 돌면서 가능한 두 숫자의 조합을 찾아야 함

⇒ Two Pointers 알고리즘 활용

5. 풀이방식

  1. 데이터 전처리 : 배열 오름차순 정렬
  2. 배열 뒤에서부터 돌면서 가장 큰 값 고정시켜줌
  3. 가장 큰 값이 고정된 상태에서 나머지 두 값 찾기
    1. right값을 기준으로 가능한 최소 left값을 찾아서 그 사이의 값들은 해당 right값과 무조건 조건에 부합한다는 방식으로 찾기

어려웠던 점

  1. right값과 left값을 찾음에 있어서도 한 값을 기준으로 생각해야하는데, 처음에 두 개 모두 기준으로 생각해서 혼란이 왔다.
  2. 중간에 더 시간복잡도를 줄이기 위해, 어느 하나의 largest_idx에서 가능한 조합이 0개가 나오면 더 이상 확인할 것도 없다는 생각에 곧바로 answer를 return해줬는데, [3, 19, 22, 24, 35, 82, 84] 와 같은 경우도 있을 수 있기 때문에 해당조건은 걸어두면 안 되었다.

느낀점

항상 느끼는 거지만, 코딩테스트 문제는 부지런히 풀어야 익숙해지는 것 같다.

내 코드

#largest_idx 주어질 때, 가능한 조합 찾는 함수
def find_possible_triangles(largest_idx, nums):
    #가장 큰 값의 idx가 2미만이면 return
    if largest_idx < 2: return 0
    #largest 바로 앞 숫자 2개 더해서 largest보다 작으면 바로 0 return
    if nums[largest_idx-1]+nums[largest_idx-2] < nums[largest_idx]:
        return 0
    #가장 큰 값 지정
    largest_num = nums[largest_idx]
    #right, left idx 지정
    right = largest_idx-1
    left = 0
    res = 0
    #left가 right보다 작은 한 반복
    while left < right:
        #left값과 right값이 largest보다 큰 최소 right위치 찾기
        #어느 정도까지 가능할지 확인하기 위해 right값 내리기
        if nums[left] + nums[right] > largest_num:
            #해당 right값과 가능한 left값 조합은 현재 left 사이의 값은 모두 가능(오름차순)
            res += (right - left)
            right -= 1
        #반대의 경우엔 left값을 올려보자
        else: left += 1
    return res

def solution(nums):
    answer =0
    #오름차순 정렬
    nums.sort()

    #뒤에서부터 largest num 고정 후 나머지 두 값 찾기
    for i in range(len(nums)-1, -1, -1):
        res = find_possible_triangles(i, nums)
        answer += res
    return answer

print(solution(nums))

+ Recent posts