(학습정리 자료)   Front-End 테스트에 대한 학습정리  Lighthouse 학습정리  크롬개발자 도구 학습정리

검사 결과

화면

검사화면

Lighthouse 결과

Lighthouse결과

접근성 검사 상세

성능 검사 점수 상세

성능점수 상세

성능 검사 추천 및 진단

추천 및 진단

성능 패널

성능패널1
성능패널2

검사결과 분석

성능 부문 개선사항

[로고이미지 width, height 지정 필요]

왜 로고이미지 width, height가 필요한 걸까?

  • 화면의 랜더링 과정에서, 레이아웃 계산은 한 번만 일어나지 않는다.
  • 레이아웃 과정에서 요소들은 다른 요소들의 배치에도 영향을 받고 재계산 과정이 일어나므로, 보통 랜더링 절차 중 많은 비용이 든다.
  • 이미지의 경우, 이미지가 다운로드되기 시작하고 브라우저가 크기를 결정할 수 있을 때만 이미지에 대한 공간을 할당할 수 있기 때문에, 이미지가 로드되면 각 이미지가 화면에 나타날 때 페이지의 재배치가 발생한다.
  • Cumulative Layout Shift(누적 레이아웃 이동, CLS) 의 발생원인
    • 크기가 정해지지 않은 이미지
    • 크기가 정해지지 않은 광고, 임베드 및 iframe
    • 동적으로 주입된 콘텐츠
    • FOIT/FOUT을 유발하는 웹 글꼴
    • DOM을 업데이트하기 전에 네트워크 응답을 대기하는 작업
  • 참고자료1, 참고자료2

개선방향

이미지 요소에 width및 height크기 속성을 포함시켜주자!

접근성 부문 개선사항

[버튼 이름 설정]

왜 버튼에 이름이 필요할까?

  • Screen reader(컴퓨터의 화면 낭독 소프트웨어)를 위해서이다.
  • Screen reader 사용자들은 접근가능한 이름이 없는 role="link", role="button", role="menuitem" 요소의 목적을 구분할 수 없다.
  • 버튼에는 화면 판독기 사용자를 위한 대상, 목적, 기능 또는 작업을 명확하게 설명하는 식별 가능한 텍스트가 필요하다.

개선방향

아래의 button-name 규칙을 참고하여, 버튼에 네이밍을 해주자!

  • button-name 규칙
    각 button요소와 요소 role="button"에 다음 특성 중 하나가 있는지 확인한다.
    • 화면 판독기 사용자가 식별할 수 있는 내부 텍스트
    • 비어 있지 않은 aria-label속성
    • aria-labelledby 을 이용하여, 스크린 리더 사용자가 식별할 수 있는 텍스트가 있는 요소를 가리킴
    • role="presentation"또는 role="none"(ARIA 1.1) 단, tab order가 아님(tabindex="-1")
    5가지 markup pattern 예시
    <button id="al" aria-label="Name"></button>
    <button id="alb" aria-labelledby="labeldiv"></button>
    <div id="labeldiv">Button label</div>
    <button id="combo" aria-label="Aria Name">Name</button>
    <button id="buttonTitle" title="Title"></button>
  • <button id="text">Name</button>
  • 참고자료

 

왜 반응형 레이아웃 적용이 필요했나?

진행하고 있는 프로젝트의 경우, 프로필박스에서 코드를 보여주는 것이 중요하기에 사이즈가 무한정 늘어나거나 줄어들어 프로필박스의 코드를 알아보는데 지장을 주는 것은 프로젝트가 제공하는 서비스에 올바를 방향이 아니었고 미관상으로도 좋지 않았다.

[기존 해결방식]

해결한 방법?

이에, css적으로 해결하려 하였다.

화면 사이즈에 맞게 width를 계산하도록 하였고 minWidth 값과 maxWidth값을 주어 해결하려고 했다.

여전히 남아있었던 문제점?

여전히 디자인 상 문제점이 남아있었습니다. 사이즈가 줄어들어서 프로필박스가 한 행에 2개씩 나오게 될 경우에 프로필박스의 개수가 홀수라면, 마지막 하나의 프로필 사이즈는 다른 프로필 사이즈와 다르게(넓어짐) 나타난다는 것이었다.

해결방법?

가장 간단하게 해결할 수 있는 방법을 고민해보았다.

메인화면에서 한 행에 표현될 수 있는 프로필박스 개수의 경우는 3개, 2개, 1개 이렇게 3가지가 있고 문제가 되는 경우는 2개인 경우이다.

이에, 2개가 나오는 경우의 브라우저 사이즈를 측정하였고 window.addEventListener의 'resize'이벤트를 통해 한 행에 2개씩 표현이 되는 경우에는 빈 프로필 박스를 임의로 추가해줌으로써 간단히 추가적인 미관상 문제를 해결할 수 있었다.

/** @jsxImportSource @emotion/react */

import { useCallback, useEffect, useRef, useState } from 'react';

import Profile from './Profile';
import { singleProfileData } from './types';

import { emptyProfileBoxStyle, profileListStyle } from './styles';
import { COMMON_SIZE } from 'styles/sizes';

interface Props {
  profileData: Array<singleProfileData>;
}

const ProfileList = ({ profileData }: Props) => {
  const profileListRef = useRef<HTMLDivElement>(null);
  const [isOdd, setIsOdd] = useState<boolean>(false);
  const [isBlankNeeded, setIsBlankNeeded] = useState<boolean>(false);

  useEffect(() => {
    if (profileData.length % 2 !== 0) setIsOdd(true);
    else setIsOdd(false);
  }, []);

  const isWidthDouble = useCallback((targetWidth: number) => {
    return COMMON_SIZE.PROFILELIST_SINGLE_WIDTH < targetWidth && targetWidth < COMMON_SIZE.PROFILELIST_TRIPLE_WIDTH;
  }, []);

  const decideBlank = useCallback(() => {
    if (!profileListRef.current) return;
    if (isWidthDouble(profileListRef.current.clientWidth)) setIsBlankNeeded(true);
    else setIsBlankNeeded(false);
  }, [profileListRef.current]);

  useEffect(() => {
    window.addEventListener('resize', decideBlank);
    return () => {
      window.removeEventListener('resize', decideBlank);
    };
  }, []);

  return (
    <div css={profileListStyle} ref={profileListRef}>
      {profileData.map((data) => (
        <Profile key={`profile-${data.id}`} singleData={data} />
      ))}
      {isOdd && isBlankNeeded && <div css={emptyProfileBoxStyle} />}
    </div>
  );
};

export default ProfileList;

결과물

[최종 해결방안]

변경 사유

프로필 개수 외에도 필터링 바 등 화면 사이즈에 따라, 디자인 변경이 필요했다.
또한, 프로필박스 개수가 4개, 5개 등인 경우에 기존 방식으로는 디자인에 예외사항이 발생했다.

변경 사항

미디어쿼리를 사용하여, 반응형 레이아웃 디자인(필터링 바 등)을 좀 더 다듬었다.
프로필박스를 사이즈에 맞게 보여주는 방법도 빈 박스를 추가하는 방식에서, 미디어 쿼리에 따라 width를 조정하는 방식으로 반영하였다.

성과

불필요한 resize이벤트를 제거할 수 있었다.
다양한 프로필 개수에 대해서도 동일한 레이아웃을 보장할 수 있었다.

결과물

컴포넌트 내부의 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 사항에 대해서도 피드백 주시면 언제든 환영입니다!

+ Recent posts