utils 폴더에 들어갈 꿀 함수들

윤뿔소·2023년 10월 15일
4

CS 지식 / 다양한 팁

목록 보기
5/21
post-thumbnail

오늘은 utils 폴더에 자주 들어가는 함수들을 공유하는 시간을 갖겠습니다.

보통 utils 폴더에 넣는 코드들은 전역적으로 쓰이는 함수들을 넣습니다. 주로 코드를 포맷팅하고 중복 코드를 줄이며, 여러 부분에서 재사용할 수 있는 함수들을 말합니다. 아니면 Static한 정적인 코드들을 넣을 수도 있구요.

예를 들어 콤마를 붙이는 함수라든지, CDN을 붙이는 함수 등등이 있습니다.

Next.JS 13 App Router - TypeSctipt 기준입니다.

포맷팅 함수

포맷팅 함수는 가장 기본적인 유틸 함수입니다. 보통 숫자, 문자열, 날짜 등을 원하는 모양대로 만들기 위한 유틸 함수가 포함됩니다.

1. 숫자 콤마 넣기

숫자에 콤마를 넣는 것은 가장 기본적이고 자주 사용되는 유틸 함수라고 할 수 있습니다. 주로 두 가지 방법이 사용됩니다.

toLocaleString()

JavaScript의 기본 내장 함수인 toLocaleString()을 사용하는 방법입니다. 가장 간편하고 빠른 방법입니다.

export default function formatNumberWithCommas(number: number | undefined) {
  if (!number) {
    return 0;
  }
  return number.toLocaleString();
}

포맷팅 함수 formatNumberWithCommas를 작성해 보았습니다.
기본값과 undefined를 처리해 undefined 에러가 발생하지 않도록 조건을 주었고, return.toLocaleString()을 적용했습니다.
안정성을 더 높이고 싶다면 추가로 파라미터인 number에 타입 검사를 해도 좋습니다.

.toLocaleString() 특성상 현지화가 잘 되어 있기 때문에 현지화를 원하거나 시간이 부족하다면 .toLocaleString()을 사용해 보시기 바랍니다.

정규식

이번에는 toLocaleString()보다는 복잡한 방법인 정규식을 사용하는 방법이 있습니다.

export default function formatNumberWithCommas(number: number | undefined) {
  if (!number || !/^[0-9,]/.test(String(number))) {
    return 0;
  }
  return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

위 함수는 추가로 test를 통해 number가 아닌 다른 타입의 데이터가 들어오면 0으로 반환되도록 해두었습니다.

toLocaleString()보다는 복잡한 방법이지만 확실하게 고정해둬 현지화별로 변하는 것을 원하지 않거나 정규식으로 확실하게 고정된 반환값을 원한다면 정규식 방법을 사용해 보시기 바랍니다.

2. 날짜 포맷팅 함수

날짜 포맷팅도 많이 사용됩니다. 리스트에서 일시 및 일자 등 사용할 곳이 많기 때문에 미리 만들어 두면 매우 편리한 함수입니다.

export function formatDateWithTime(inputDateString: string) {
  const inputDate = new Date(inputDateString);
  const year = inputDate.getFullYear();
  const month = String(inputDate.getMonth() + 1).padStart(2, '0');
  const day = String(inputDate.getDate()).padStart(2, '0');
  const hours = String(inputDate.getHours()).padStart(2, '0');
  const minutes = String(inputDate.getMinutes()).padStart(2, '0');
  const formattedDate = `${year}.${month}.${day} ${hours}:${minutes}`;
  return formattedDate;
}

export function formatDate(inputDateString: string) {
  const inputDate = new Date(inputDateString);
  const year = inputDate.getFullYear();
  const month = String(inputDate.getMonth() + 1).padStart(2, '0');
  const day = String(inputDate.getDate()).padStart(2, '0');
  const formattedDate = `${year}.${month}.${day}`;
  return formattedDate;
}

날짜를 사용하는 곳이 일자, 일시 두 가지이기 때문에 날짜와 시간이 함께 있는 formatDateWithTime과 날짜만 있는 formatDate를 만들었습니다.
각각 입력받은 inputDateString: string 파라미터를 new Date(inputDateString)로 새 Date 데이터를 만듭니다.
그다음 각각 원하는 년, 월, 일, 시, 분으로 나누어 정규식을 사용해 연결하고 그 연결한 데이터를 반환하는 방식입니다.

추가로 padStart 함수는 JS 내장 메소드 함수로, 첫 번째 파라미터보다 길이가 작은 문자열이 있다면 첫 번째 파라미터만큼 왼쪽부터 두 번째 파라미터로 채워주는 함수입니다.
그래서 한 자리 숫자가 들어오면 0으로 채워주기 위해 사용했습니다.

만약 YY-MM-DDYY.MM.DD HH:MM:SS 같은 형식을 원하신다면 각 부분을 선언한 후 정규식으로 이어주고 반환하면 됩니다!

3. 전화번호 포맷팅 함수

export default function formatPhoneNumber(phoneNumber: string | undefined | null): string {
  if (phoneNumber) {
    // 모든 숫자 제외하고 제거
    const cleaned = phoneNumber.replace(/[^0-9]/g, '');
    // 형식에 맞게 '-' 삽입
    let formatted = '';
    if (cleaned.length === 10) {
      formatted = cleaned.replace(/(\d{3})(\d{3,4})(\d{4})/, '$1-$2-$3');
    } else if (cleaned.length === 11) {
      formatted = cleaned.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
    } else if (cleaned.length === 9) {
      formatted = cleaned.replace(/(\d{2})(\d{3})(\d{4})/, '$1-$2-$3');
    } else {
      // 기타 길이의 번호는 그대로 반환
      formatted = cleaned;
    }
    return formatted;
  }
  return '-';
}

이 함수는 입력된 전화번호에서 숫자 이외의 모든 문자를 제거한 후, 원하는 형식에 맞게 하이픈(-)을 삽입해 반환합니다.

전화번호의 길이가 다양하기 때문에, 길이가 9, 10, 11자리인 경우를 처리하고, 나머지는 원래 번호를 반환합니다.
이를 통해 일관성과 재사용성을 높일 수 있습니다.

유효성 검사 함수

프로젝트에서 여러 곳에서 유효성 검사가 필요하다면, 이를 유틸리티 함수로 만들어 재사용해 더욱 고도화된 프로젝트를 만들 수 있습니다.

여기서는 제가 자주 사용했던 몇 가지 유효성 검사 함수를 작성해보도록 하겠습니다.

1. 이메일 유효성 검사

이메일 주소의 유효성을 검사하는 함수입니다. 기본적인 이메일 형식을 확인합니다.

export function isEmailValid(email: string): boolean {
  const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
  return emailRegex.test(email);
}

간단한 정규식을 사용해 이메일 주소의 형식이 올바른지 확인합니다. @ 기호와 도메인이 포함되어 있는지, 그리고 기본적인 이메일 구조를 따르는지 검사합니다.

2. 비밀번호 유효성 검사

비밀번호의 복잡성을 확인하는 함수입니다. 여기서의 조건은 영문, 숫자, 특수문자를 포함한 8자 이상의 비밀번호를 요구합니다.

export function isPasswordValid(password: string): boolean {
  const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
  return passwordRegex.test(password);
}

정규식의 의미는 아래와 같습니다.

  • 최소 8자 이상
  • 적어도 하나의 영문자 포함
  • 적어도 하나의 숫자 포함
  • 적어도 하나의 특수문자(@$!%\*#?&) 포함

3. 닉네임 유효성 검사

사용자 닉네임의 유효성을 검사하는 함수입니다. 여기서의 조건은 2-10자의 한글, 영문, 숫자 조합을 허용합니다.

export function isNicknameValid(nickname: string): boolean {
  const nicknameRegex = /^[가-힣a-zA-Z0-9]{2,10}$/;
  return nicknameRegex.test(nickname);
}

정규식의 의미는 아래와 같습니다.

  • 한글, 영문 대소문자, 숫자만 허용
  • 최소 2자, 최대 10자

4. 휴대폰 전화번호 유효성 검사 (한국)

한국 휴대폰 전화번호 유효성을 검사하는 함수입니다. 여기서의 조건은 숫자로 이뤄지고 3-(3,4)-4의 형태를 띄는 문자열을 허용합니다.

export function isPhoneNumberValid(phoneNumber: string): boolean {
  const phoneRegex = /^(01[016789]{1})-?[0-9]{3,4}-?[0-9]{4}$/;
  return phoneRegex.test(phoneNumber);
}

정규식의 의미는 아래와 같습니다.

  • 각 문자열을 하이픈-으로 나눔, 있을 수도 있고 없을 수도 있음
  • 하이픈으로 나눈 각 칸은 3, 3 or 4, 4자리로 와야하고, 전부 숫자임
  • 모든 휴대폰 번호는 01로 시작, 그 다음엔 0, 1, 6, 7, 8, 9 중 하나가 와야함

함수 조합

각 함수는 입력값이 유효한지 여부를 boolean 값으로 반환합니다. 이 조건을 조합해 회원가입, 로그인, 사용자 정보 수정 등에 사용하면 됩니다.

function handleSignUp(email: string, password: string, nickname: string) {
  if (!isEmailValid(email)) {
    alert('올바른 이메일 주소를 입력해주세요.');
    return;
  }

  if (!isPasswordValid(password)) {
    alert('비밀번호는 8자 이상이며, 영문, 숫자, 특수문자를 포함해야 합니다.');
    return;
  }

  if (!isNicknameValid(nickname)) {
    alert('닉네임은 2-10자의 한글, 영문, 숫자로만 구성되어야 합니다.');
    return;
  }

  // 유효성 검사를 모두 통과하면 회원가입 로직 실행
  // ...
}

이런 식으로 유효성 검사 함수를 만들어 사용하면 재사용성을 높이고 일관된 검증 로직을 유지/보수할 수 있습니다.
프로젝트의 요구사항에 따라 정규식을 수정하거나 추가적인 검증 로직을 넣을 수 있으니, 필요에 맞게 커스터마이징하시면 됩니다.

네트워크 요청 함수

API를 통해 통신하려면 fetch API, axios 등을 사용합니다. 하지만 여러 번 사용되거나 중복이 많다면, 네트워크 요청 함수도 유틸리티 함수로 만들어 재사용성을 높일 수 있습니다.

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

interface RequestOptions<T> {
  method: HttpMethod;
  headers?: { [key: string]: string };
  body?: T;
}

async function sendRequest<T>(url: string, options: RequestOptions<T>): Promise<T> {
  try {
    const response = await fetch(url, {
      method: options.method || 'GET',
      headers: options.headers,
      body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
    });
    if (!response.ok) {
      throw new Error(`HTTP Error! Status: ${response.status}`);
    }
    // 원하는 데이터 형식으로 변환 (예: JSON)
    const data: T = await response.json();
    return data;
  } catch (error) {
    throw error;
  }
}

// GET 요청 예시
const getResponse = await sendRequest<{ exampleData: string }>('https://api.example.com/data', {
  method: 'GET',
});

// POST 요청 예시
const postData = { key: 'value' };
const postResponse = await sendRequest<{ responseMessage: string }>('https://api.example.com/post', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: postData,
});

이런 식으로 사용할 수 있습니다. 보통 GET은 옵션이 별로 없으니 메서드가 undefined일 시 GET으로 설정해 두었습니다.
Next.js App Router를 사용하고 있어 axios를 사용하지 않고 기본적으로 fetch를 추천하는 편이라 fetch API로 작성해 보았습니다.
이런 식으로 하면 중복 코드를 줄일 수 있어 간편하게 사용할 수 있습니다.

여기에 401 처리, 리액트 쿼리, Interceptor 개념 등이 들어간다면 달라질 수 있습니다. 자신의 프로젝트에 맞게 수정해 보시기 바랍니다!

로컬스토리지 토큰 핸들 함수

웹에서 가장 많이 사용하는 인증 수단 중 하나인 JWT를 많이 사용하면서 유틸리티 함수에서도 토큰 핸들을 다룰 수 있습니다.

import { accessTokenStore } from '#/store/auth/access-token-store';

export const saveRefreshTokenToLocalStorage = (refreshToken: string) => {
  if (typeof window !== 'undefined') {
    localStorage.setItem('refreshToken', refreshToken);
  }
};

export const getRefreshTokenFromLocalStorage = () => {
  if (typeof window !== 'undefined') {
    return localStorage.getItem('refreshToken') || '';
  }
};

export const saveAccessTokenToZustand = (accessToken: string) => {
  accessTokenStore.getState().setAccessToken(accessToken);
};

export const getAccessTokenFromZustand = () => {
  return accessTokenStore.getState().accessToken;
};

리프레시 토큰은 Local Storage에, 액세스 토큰은 외부에 노출되지 않도록 로컬 전역 상태 라이브러리인 Zustand에 저장하는 방식으로 유틸리티 함수를 작성해 보았습니다.

Next.js-TypeScript를 사용할 때 그냥 localStorage를 사용하면 타입 에러가 발생하는 것을 볼 수 있습니다.
그래서 조건에 typeof window !== 'undefined'를 작성해 넣고, 타입 에러가 나지 않도록 했습니다. 하지만 이렇게 사용하면 모바일에서 에러를 일으킨다고 합니다. 일단 여기서는 주제에서 벗어나므로 넘어가겠습니다.

또한 Zustand를 사용해 유틸리티 함수를 만들었습니다. 이렇게 하면 간편하게 함수 하나로 조작하고, 불러올 수 있습니다.

디바운스/스로틀 함수

디바운스와 스로틀은 성능 최적화를 위해 자주 사용되는 기술입니다. 이 두 기술은 이벤트 핸들러가 많은 연산을 수행하는 상황에 아주 유용합니다.

1. Debounce

export function debounce<F extends (...args: any[]) => any>(func: F, waitFor: number) {
  let timeout: ReturnType<typeof setTimeout> | null = null;

  return (...args: Parameters<F>): void => {
    if (timeout !== null) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => func(...args), waitFor);
  };
}

연이어 호출되는 함수들 중에서 마지막 함수(또는 제일 처음)만 호출되도록 하는 기능입니다.

작동 방식

  1. 함수 호출이 시작되면 타이머를 설정합니다.
  2. 설정된 시간(waitFor) 이내에 다시 함수가 호출되면, 이전 타이머를 취소하고 새로운 타이머를 설정합니다.
  3. 설정된 시간 동안 함수 호출이 없으면, 마지막으로 전달된 인자로 함수를 실행합니다.

사용 상황

  • 검색 입력 필드의 자동완성 기능
  • 창 크기 조절(resize) 이벤트 처리
  • 버튼 중복 클릭 방지

사용 예시

const debouncedSearch = debounce((query: string) => {
  // 검색 API 호출
  console.log('Searching for:', query);
}, 300);

// 사용자가 입력할 때마다 호출되지만, 실제 검색은 마지막 입력 후 300ms 후에 실행
inputElement.addEventListener('input', (e) => debouncedSearch(e.target.value));

2. Throttle

export function throttle<F extends (...args: any[]) => any>(func: F, limit: number) {
  let inThrottle: boolean;
  return function (this: any, ...args: Parameters<F>): void {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

일정 시간 간격으로 함수가 최대 한 번만 실행되도록 하는 기능입니다.

작동 방식

  1. 함수가 호출되면 즉시 실행됩니다.
  2. 이후 설정된 시간 : limit 동안은 함수 호출을 무시합니다.
  3. 설정된 시간이 지나면 다시 함수 호출이 가능해집니다.

사용 상황

  • 스크롤 이벤트 처리
  • 게임에서의 공격 속도 제어
  • 분당 요청 수 제한 등의 API 요청 제한

사용 예시

const throttledScroll = throttle(() => {
  // 스크롤 위치에 따른 작업 수행
  console.log('Scroll position:', window.scrollY);
}, 1000);

// 스크롤 이벤트가 빈번히 발생해도 1초에 최대 한 번만 실행
window.addEventListener('scroll', throttledScroll);

난수 생성 유틸리티 함수

난수 생성은 다양한 상황에서 유용하게 사용됩니다. 여기서 다양한 난수 생성 유틸 함수를 보여드리겠습니다.

1. 기본 난수 생성 함수

export function generateRandomNumber(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

이 함수는 주어진 범위 내에서 정수 난수를 생성합니다. 하지만 반쪽짜리 난수인데, Math.random()은 예측 가능, 정확도 부족 등의 취약점을 갖고 있기 때문입니다.

사용 상황

개발 환경 등 정말 중요하지 않은 상황에서 쓰면 되겠습니다.

  • 주사위 굴리기 시뮬레이션
  • 랜덤 아이템 선택
  • 간단한 게임 로직에서의 난수 사용

2. UUID 생성 함수

UUID는 고유한 식별자로 자주 사용됩니다.

import { v4 as uuidv4 } from 'uuid';

export function generateUuid() {
  const tokens = uuidv4().split('-');
  return tokens[2] + tokens[1] + tokens[0] + tokens[3] + tokens[4];
}

이 함수는 uuid 라이브러리를 사용해 인증된, 표준 UUID 형식 문자열을 생성합니다.
uuid 라이브러리는 한계가 있는데 기본적으로 속도와 사용성을 위해 암호학적으로 안전한 난수를 보장하지 않습니다.

사용 상황

  • 데이터베이스 레코드의 고유 식별자 생성
  • 세션 ID 생성
  • 임시 파일명 생성

3. 암호학적으로 안전한 난수 생성 함수

보안이 중요한 상황에서는 uuid 대신 crypto를 사용하는 것이 좋습니다.

export function generateSecureRandomNumber(min: number, max: number): number {
  const range = max - min + 1;
  const bytesNeeded = Math.ceil(Math.log2(range) / 8);
  const randomBytes = new Uint8Array(bytesNeeded);
  const maximumRange = 256 ** bytesNeeded;
  const randomValue = () => {
    window.crypto.getRandomValues(randomBytes);
    let value = 0;
    for (let i = 0; i < bytesNeeded; i++) {
      value = value * 256 + randomBytes[i];
    }
    return value;
  };

  let result = randomValue();
  while (result >= maximumRange - (maximumRange % range)) {
    result = randomValue();
  }
  return min + (result % range);
}

이 함수는 암호학적으로 안전한 난수를 생성합니다.

사용 상황

  • 암호화 키 생성
  • 보안 토큰 생성
  • 중요한 금융 거래에서의 난수 사용

그 외에도 Provider 제공 함수, 암호화-복호화 및 해싱 함수, Fisher-Yates 알고리즘을 사용한 배열 셔플 함수 등을 만들어서 utils 폴더에 넣어둘 수 있습니다.

또한 유틸리티 함수들을 모아놓은 Lodash, Underscore, Ramda, Remeda 등을 사용하는 것도 또 다른 방법입니다.

보통 한 개의 프로젝트에만 사용되는 함수가 아니기 때문에 새 프로젝트를 시작하면 먼저 utils 폴더를 복사해 바로 붙여두는 편입니다. 이 글을 읽는 여러분도 자신만의 utils 폴더를 만들어 프로젝트 수행 능력을 향상시켜 보시면 좋겠습니다.

profile
코뿔소처럼 저돌적으로

6개의 댓글

comment-user-thumbnail
2023년 10월 19일

꿀팁 많이 알아갑니당 ㅎㅎ

답글 달기
comment-user-thumbnail
2023년 10월 20일

앗 요즘 저도 자주 쓰는 함수들 util 분리 해놓는 작업 함께 진행하고 있어서 더 와닿았어요! 잘 보고 갑니당

답글 달기
comment-user-thumbnail
2023년 10월 22일

태연님꺼 보고 공부 많이 해야할 것 같습니다.. 후..

답글 달기
comment-user-thumbnail
2023년 10월 22일

저도 분리를 잘 해야하는데 연습을 많이 해둬야겠어요 ㅎㅎ ㅠㅠ

답글 달기
comment-user-thumbnail
2023년 10월 22일

오오 개인 프로젝트할 때 참고하면 좋을 것 같아요..!!

답글 달기
comment-user-thumbnail
2023년 10월 22일

지인들 프로젝트 보면 실제로 많이 넣는 함수들 같아요! 꿀팁! 감사합니다~!

답글 달기