오늘은 utils
폴더에 자주 들어가는 함수들을 공유하는 시간을 갖겠습니다.
보통 utils 폴더에 넣는 코드들은 전역적으로 쓰이는 함수들을 넣습니다. 주로 코드를 포맷팅하고 중복 코드를 줄이며, 여러 부분에서 재사용할 수 있는 함수들을 말합니다. 아니면 Static한 정적인 코드들을 넣을 수도 있구요.
예를 들어 콤마를 붙이는 함수라든지, CDN을 붙이는 함수 등등이 있습니다.
Next.JS 13 App Router - TypeSctipt 기준입니다.
포맷팅 함수는 가장 기본적인 유틸 함수입니다. 보통 숫자, 문자열, 날짜 등을 원하는 모양대로 만들기 위한 유틸 함수가 포함됩니다.
숫자에 콤마를 넣는 것은 가장 기본적이고 자주 사용되는 유틸 함수라고 할 수 있습니다. 주로 두 가지 방법이 사용됩니다.
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()
보다는 복잡한 방법이지만 확실하게 고정해둬 현지화별로 변하는 것을 원하지 않거나 정규식으로 확실하게 고정된 반환값을 원한다면 정규식 방법을 사용해 보시기 바랍니다.
날짜 포맷팅도 많이 사용됩니다. 리스트에서 일시 및 일자 등 사용할 곳이 많기 때문에 미리 만들어 두면 매우 편리한 함수입니다.
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-DD
나 YY.MM.DD HH:MM:SS
같은 형식을 원하신다면 각 부분을 선언한 후 정규식으로 이어주고 반환하면 됩니다!
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자리인 경우를 처리하고, 나머지는 원래 번호를 반환합니다.
이를 통해 일관성과 재사용성을 높일 수 있습니다.
프로젝트에서 여러 곳에서 유효성 검사가 필요하다면, 이를 유틸리티 함수로 만들어 재사용해 더욱 고도화된 프로젝트를 만들 수 있습니다.
여기서는 제가 자주 사용했던 몇 가지 유효성 검사 함수를 작성해보도록 하겠습니다.
이메일 주소의 유효성을 검사하는 함수입니다. 기본적인 이메일 형식을 확인합니다.
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);
}
간단한 정규식을 사용해 이메일 주소의 형식이 올바른지 확인합니다. @
기호와 도메인이 포함되어 있는지, 그리고 기본적인 이메일 구조를 따르는지 검사합니다.
비밀번호의 복잡성을 확인하는 함수입니다. 여기서의 조건은 영문, 숫자, 특수문자를 포함한 8자 이상의 비밀번호를 요구합니다.
export function isPasswordValid(password: string): boolean {
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
return passwordRegex.test(password);
}
정규식의 의미는 아래와 같습니다.
@$!%\*#?&
) 포함사용자 닉네임의 유효성을 검사하는 함수입니다. 여기서의 조건은 2-10자의 한글, 영문, 숫자 조합을 허용합니다.
export function isNicknameValid(nickname: string): boolean {
const nicknameRegex = /^[가-힣a-zA-Z0-9]{2,10}$/;
return nicknameRegex.test(nickname);
}
정규식의 의미는 아래와 같습니다.
한국 휴대폰 전화번호 유효성을 검사하는 함수입니다. 여기서의 조건은 숫자로 이뤄지고 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);
}
정규식의 의미는 아래와 같습니다.
-
으로 나눔, 있을 수도 있고 없을 수도 있음각 함수는 입력값이 유효한지 여부를 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를 사용해 유틸리티 함수를 만들었습니다. 이렇게 하면 간편하게 함수 하나로 조작하고, 불러올 수 있습니다.
디바운스와 스로틀은 성능 최적화를 위해 자주 사용되는 기술입니다. 이 두 기술은 이벤트 핸들러가 많은 연산을 수행하는 상황에 아주 유용합니다.
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);
};
}
연이어 호출되는 함수들 중에서 마지막 함수(또는 제일 처음)만 호출되도록 하는 기능입니다.
waitFor
) 이내에 다시 함수가 호출되면, 이전 타이머를 취소하고 새로운 타이머를 설정합니다.const debouncedSearch = debounce((query: string) => {
// 검색 API 호출
console.log('Searching for:', query);
}, 300);
// 사용자가 입력할 때마다 호출되지만, 실제 검색은 마지막 입력 후 300ms 후에 실행
inputElement.addEventListener('input', (e) => debouncedSearch(e.target.value));
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);
}
};
}
일정 시간 간격으로 함수가 최대 한 번만 실행되도록 하는 기능입니다.
limit
동안은 함수 호출을 무시합니다.const throttledScroll = throttle(() => {
// 스크롤 위치에 따른 작업 수행
console.log('Scroll position:', window.scrollY);
}, 1000);
// 스크롤 이벤트가 빈번히 발생해도 1초에 최대 한 번만 실행
window.addEventListener('scroll', throttledScroll);
난수 생성은 다양한 상황에서 유용하게 사용됩니다. 여기서 다양한 난수 생성 유틸 함수를 보여드리겠습니다.
export function generateRandomNumber(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
이 함수는 주어진 범위 내에서 정수 난수를 생성합니다. 하지만 반쪽짜리 난수인데, Math.random()
은 예측 가능, 정확도 부족 등의 취약점을 갖고 있기 때문입니다.
개발 환경 등 정말 중요하지 않은 상황에서 쓰면 되겠습니다.
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
라이브러리는 한계가 있는데 기본적으로 속도와 사용성을 위해 암호학적으로 안전한 난수를 보장하지 않습니다.
보안이 중요한 상황에서는 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
폴더를 만들어 프로젝트 수행 능력을 향상시켜 보시면 좋겠습니다.
꿀팁 많이 알아갑니당 ㅎㅎ