[1차 프로젝트/리팩토링] 상수화/유틸화/api (최근 검색어 기능)

샘·2023년 5월 3일

프로젝트

목록 보기
5/9
post-thumbnail

🖥 개요

엘리스트랙 1차 프로젝트 당시 진행했던 바닐라JS로 쇼핑몰 구축 프로젝트에서 네비게이션 중 글로벌 검색 기능 및 최근 검색어 기능을 구현했었다.

검색 페이지 자체는 다른 팀원분께서 만들어두신 것이 있어서 검색 기능은 금방 구현하였으나 이번 프로젝트에서 로컬스토리지를 활용해 보고 싶었기 때문에! 코치님께 추천받은 최근 검색어 기능을 추가해 보기로했다.

이 기능은 최종 프로젝트 발표일 하루 전 저녁에 사용성 부분에서 포인트가 있었으면 좋겠다는 피드백을 받고 급하게 구현한 기능이라 모든 것이 하드코딩 되어있었다. 때문에 코드도 길고.. 가독성도 좋지 않은 일회성 코드들로 도배되어 버렸다..😭

이번 리팩토링에서는 컴포넌트들의 로직에 대한 개선보다는 하드코딩 된 함수들을 상수화, 유틸화, api로 만들어 추후 재사용하기 용이하도록 개선해보았다.

😅 기존 코드

  • 코드가 너무 길고 지저분해서 이번 리팩토링과 관련없는 부분은 생략하고 넘어가겠습니다.
//Search.js

//텍스트로 검색하고 검색어를 최근 검색어에 저장하는 기능
function searchByTerm(event) {
  event.preventDefault();
  let inputValue;
  if (event.target.tagName === 'LI') {
    inputValue = event.target.firstChild.textContent;
    if (inputValue !== '') {
      window.location.href = `/products/?team=all&category=all&sort=recent&search=${inputValue}`;
    }
  } else {
    const input = document.querySelector('.search-content');
    inputValue = input.value;
    if (inputValue !== '') {
      window.location.href = `/products/?team=all&category=all&sort=recent&search=${inputValue}`;

      let recentSearches =
        JSON.parse(localStorage.getItem('recentSearches')) || [];
      recentSearches.unshift(inputValue);
      recentSearches = Array.from(new Set(recentSearches));
      localStorage.setItem('recentSearches', JSON.stringify(recentSearches));
    }
  }
}

//최근 검색어를 삭제하는 기능
function deleteSearchTerm(event) {
  ...
  let recentSearches = JSON.parse(localStorage.getItem('recentSearches')) || [];
  recentSearches = recentSearches.filter(term => term !== termToDelete);
  localStorage.setItem('recentSearches', JSON.stringify(recentSearches));
  renderRecentTerms(event);
}

//전체 최근 검색어를 삭제하는 기능
function deleteAllTerms() {
  localStorage.setItem('recentSearches', JSON.stringify(''));
}

//최근 검색어 컨테이너 외부를 클릭할 시 돔요소가 삭제되는 기능
function removeRecentTermsContainer(event) {
	...
}

//검색어 폼을 클릭할 시 최근 검색어 컨테이너를 렌더링 하는 기능
function renderRecentTerms(event) {
	...
    
  //최근 검색어가 있다면 렌더링되는 조건  
  const recentSearches =
    JSON.parse(localStorage.getItem('recentSearches')) || [];
  if (recentSearches.length > 0) {
    //최근 검색어 컨테이너 관련 돔 요소들을 그리는 코드
    	...
    });
	...
  }
}

//검색 컴포넌트를 렌더링하는 기능
export function SearchBox(target) {
	...
}

문제점

  • 자주 쓰이는 localStorage와 window.location.href를 하드코딩
    => 실수/버그 발생 가능성이 있고 코드가 길어지고 중복되는 코드가 많아졌다.
  • 여러 기능을 한 함수 내에서 처리하고 있어 코드의 가독성이 낮다.
    => 가령 searchByTerm() 함수에 검색 후 링크를 이동하는 로직도 있고, 검색어를 로컬스토리지에 저장하는 기능도 있는 것..

🧐 개선 코드

1. URL 상수화

//constants.js
export const ROUTES = {
  HOME: '/',
  LOGIN: '/login',
  LOGIN_AUTH: '/login/auth',
  FIND_PASSWORD: '/find-password',
  FIND_PASSWORD_RESET: '/find-password/reset',
  SIGNUP: '/signup',
  PRODUCTS: '/products',
  PRODUCT_DETAIL: '/products/:id',
  CART: '/cart',
  ORDER: '/order',
  ORDER_COMPLETE: '/order/complete',
  LOGIN: '/login',
  ...
};
  • 상수를 사용하면 상수 값이 변경될 경우, 해당 값만 변경하면 되므로 코드 수정이 쉬워진다.
    => 실무에서 이런 방식으로 URL들을 관리한다고 말씀주셨다.

2. URL 생성 및 리턴 유틸화

//utils.js
function generateUrl(path, params = {}, method = 'pathVariable') {
  let url = '';
  if (method === 'pathVariable') {
    
    //1) 
    url = path;
    for (const [key, value] of Object.entries(params)) {
      url = url.replace(`:${key}`, value);
    }
  } else if (method === 'queryString') {
    
    //2) 
    const urlObject = new URL(path, window.location.origin);
    Object.entries(params).forEach(([key, value]) => {
      urlObject.searchParams.set(key, value);
    });
    url = urlObject.toString();
  }
  return url;
}

//3) 
export function navigateTo(path, params = {}, method = 'pathVariable') {
  const url = generateUrl(path, params, method);
  window.location.href = url;
}

1) generateUrl() 함수에서 method는 URL을 생성하는 방법을 지정하는 매개변수다. 'pathVariable'으로 설정된 경우 path 문자열에 존재하는 :key 형태의 템플릿 변수를 params 객체에서 해당 key의 값으로 치환하여 URL을 생성한다.
generateUrl('/user/:id', { id: '123' })가 호출되면 /user/123이 반환된다.

2)method 매개변수가 'queryString'인 경우, path 문자열에 쿼리스트링을 포함할 수 있다. params 객체에 있는 키-값 쌍은 쿼리스트링으로 변환되어 URL에 추가된다. 이때 new URL() 생성자를 사용하여 URL 객체를 생성한 뒤, searchParams 프로퍼티를 사용하여 URL 쿼리스트링을 설정한다.
generateUrl('/users', { page: 1, limit: 10 })가 호출되면 현재 도메인의 /users?page=1&limit=10 URL이 반환된다.

3) navigateTo() 함수에서는 generateUrl() 함수를 호출하여 생성된 URL로 브라우저를 이동시키기 위해 window.location.href를 변경한다.

3. getItem, setItem 유틸화

//utils.js
export function getItem(key) {
  return JSON.parse(localStorage.getItem(key));
}

export function setItem(key, value) {
  return localStorage.setItem(key, JSON.stringify(value));
}

setItem('recentSearches', '['유니폼', '머리띠']') 함수를 사용하여 name이라는 키에 John이라는 값을 저장하면, getItem('recentSearches') 함수를 사용하여 해당 값을 다시 불러올 수 있다.

4. 최근 검색어 get, create, delete 기능 searchAPI 파일에 분리

//searchAPI.js
import { getItem, setItem } from '/js/utils.js';

//1) 
const RECENT_SEARCHES = 'recentSearches';

//2) 
export function getRecentSearches() {
  return getItem(RECENT_SEARCHES) ?? [];
}

//3) 
export function createRecentSearch(inputValue) {
  const newRecentSearches = Array.from(
    new Set([inputValue, ...getRecentSearches()])
  );
  setItem(RECENT_SEARCHES, newRecentSearches);
}

//4) 
export function deleteRecentSearch(termToDelete) {
  const newRecentSearches = [...getRecentSearches()].filter(
    (term) => term !== termToDelete
  );
  setItem(RECENT_SEARCHES, newRecentSearches);
}
  
//5) 
export function deleteAllRecentSearches() {
  setItem(RECENT_SEARCHES, '');
}

1) 검색어를 저장할 localStorage 키 값 상수

2) getItem(RECENT_SEARCHES) 유틸 함수를 이용하며, 만약 해당 키로 저장된 값이 없다면 빈 배열을 반환한다.

3) 새로운 검색어를 localStorage에 추가함. 현재 localStorage에 저장된 검색어와 중복되는 값이 있다면 제외한다. setItem(RECENT_SEARCHES, newRecentSearches) 유틸 함수를 이용해 저장한다.

4) localStorage에 저장된 검색어 중, termToDelete 매개변수로 전달된 검색어를 삭제한다. filter() 함수를 이용해 termToDelete와 같지 않은 검색어만 필터링하고 setItem(RECENT_SEARCHES, newRecentSearches) 유틸함수를 이용해 저장한다.

5) localStorage에 저장된 모든 검색어를 삭제한다. setItem(RECENT_SEARCHES, '') 유틸 함수를 이용해 저장한다.

5. 상수, 유틸함수, serchAPI를 활용한 Search.js 개선 코드

//Search.js
import { ROUTES } from '/js/constants.js';
import { navigateTo } from '/js/utils.js';
import {
  getRecentSearches,
  createRecentSearch,
  deleteRecentSearch,
  deleteAllRecentSearches,
} from '/js/api/searchAPI.js';

//텍스트로 검색하고 검색어를 최근 검색어에 저장하는 기능
function searchByTerm(event) {
	...
	//중복 코드 개선 및 상수 URL과 생성/연결 util 사용
  if (inputValue !== '') {
    navigateTo(
      ROUTES.PRODUCTS,
      {
        team: 'all',
        category: 'all',
        sort: 'recent',
        search: inputValue,
      },
      'queryString'
    );
   //최근 검색어 저장 api 사용
    createRecentSearch(inputValue);
  }
}

//최근 검색어를 삭제하는 기능
function deleteSearchTerm(event) {
  ...
  //최근 검색어 삭제 api 사용
  deleteRecentSearch(termToDelete);
  renderRecentTerms(event);
}

//최근 검색어 컨테이너 외부를 클릭할 시 돔요소가 삭제되는 기능
function removeRecentTermsContainer(event) {
	...
}

//검색어 폼을 클릭할 시 최근 검색어 컨테이너를 렌더링 하는 기능
function renderRecentTerms(event) {
  //최근 검색어 불러오기 api 사용
  const recentSearches = getRecentSearches();
  
  if (recentSearches.length > 0) {
  //최근 검색어가 있다면 렌더링되는 조건  
  	...
    //전체 최근검색어 삭제 api 사용
    deleteAllTermsButton.addEventListener('click', deleteAllRecentSearches);
    ...
  }
    ...
}
    
//검색 컴포넌트를 렌더링하는 기능
export function SearchBox(target) {
 	...
}

코드를 추상화함으로써 확실히 코드가 간결해지고 읽기가 쉬워졌다!!!

💭 느낀점

이번 리팩토링을하면서 처음엔 코치님께서 리뷰 주신 URL상수화, URL생성 유틸화 정도만 진행하고 컴포넌트 생성 로직을 수정하려했는데 수정하면서 localStorage관련 함수도 묶게되었고, 포스팅하는 중간중간 묶을 수 있는 부분이 보여서 생각보다 이번 리팩토링에 시간을 많이 할애했다. 그만큼 이번 리팩토링을 통해 배운 것이 많았고 다시 한 번 빠른 기능구현만이 중요한게 아니란걸 느꼈다. (그래도 피드백 주신 부분 외에 리팩토링할 부분을 척척 찾아냈다는게 좀 성장한 것 같아서 뿌듯하다 ㅎㅎ)

이전에 개발을 모르던 PM시절엔 한 부분만 수정했는데 다른 페이지도 바뀌고 난리가나는지 이해가 안갔는데, 이런 부분이 부족해서 그랬던거였구나.. 너무 뒤늦게 깨달아버렸다. 아직 상수화한 변수, 유틸화한 함수들을 하드코딩한 다른 페이지에도 리팩토링하며 적용시키는 작업을 마무리하는 작업이 남았다. 이 작업이 끝나야지 비로소 이번 리팩토링이 유의미해지겠지...

이번 리팩토링으로 프로젝트에서 어떤 메서드나 함수들이 전역적으로 사용되는지 어느정도 파악했고, 상수화해야하는 변수도 기준이 모호했는데 갈피를 잡은 것 같다. 다음 프로젝트부터는 리팩토링하면서 코드를 수정할 생각을 하지말고 초장부터 자주 쓰는 내용들은 상수화, 유틸화하여 작업해야겠다.


다음 포스팅에서는 최근 검색 컴포넌트들을 렌더링 하는 조건을 효율적인 방법으로 변경해보고, dom선택과 컴포넌트화를 명확하게 해보려 한다.

0개의 댓글