React 검색 자동완성 - react geomsaeg jadong-wanseong

변화에 잘 대응하는 견고한 코드 작성을 목표로 했으니 실제로 변경 요청이 있을 때 본인이 작성한 코드가 변화를 잘 받아들일 수 있는지 검증해야 한다. 실무에서 으레 있을 법한 상황을 토대로 시나리오를 구성해봤다.

1-7-1 변화 상황 시나리오

기본 자동검색은 보통은 아래와 같은 view를 제공한다.

React 검색 자동완성 - react geomsaeg jadong-wanseong
그런데 이 검색 자동 완성 코드를 영화검색에 쓴다고 하는데, 영화검색에서 요구하는 결과 화면이 기존 검색완성 과는 다르다 어떻게 대응해야 하는가?

React 검색 자동완성 - react geomsaeg jadong-wanseong

1-7-2 변화 상황에 대응하기

코드의 재사용 가능성?

시나리오를 확인해보니 기본적인 기능(검색어 입력, 검색결과 네이비게이션 등)은 똑같지만 검색결과(resultView) 화면이 다르다. 그렇다면 전달받은 데이터소스를 처리하는 함수와, 처리된 데이터를 통해 화면을 그리는 템플릿 함수만 변화하면 기존에 사용했던 코드를 재사용 할 수 있다. 변화하는 내용을 config.js 내부에 resultView 객체에 있는 함수들을 변경 하면 된다. 변화를 예상하고 격리시켜 놨기 때문에 해당 변경은 다른 객체에 영향을 주지 않는다. 코드를 안심하고 변경할 수 있다.

실제 바꿔야하는 코드

아래 코드에서 템플릿 함수 (

const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


0 ,
const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


1) 와 데이터 처리 (
const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


2) 만 바꿔주면 나머지 코드는 그대로 재사용이 가능하다. 목표달성이다. 짝짝짝!!

// config.js
const global = {
	...
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
	...
};

export const inputView = {
	...
};

export const resultView = {
	// 템플릿 - 최근 검색 결과 
  recentQueryTemplate(recentQueryList) {
		...
  },
	
  // 템플릿 - 자동 완성 결과 
  suggestionTemplate(query, suggesions) {
		...
  },
	
  // 데이터 처리 - 모델로 부터 전달받은 데이터에서 자동완성 데이터를 추출하는 함수 
  getAutoSuggesionList({ dataSrc, query, config }) {
		
  },
};

2. 고급 UI 제어

* Debounce 및 Throttle 이해를 위한 참고자료

  • 아래에서는 프론트엔드 고급 UI 제어 기술인
    const global = {
      inputEl: '.autoComplete_input',
      resultEl: '.autoComplete_result',
      resultItem: 'autoComplete_result_item',
      resultItemHighlighted: 'autoComplete_result_item-highlighted'
    };
    
    export const model = {
      srcUrl: './src/data.json'
    };
    
    export const controller = {
      inputEl: global.inputEl,
      resultEl: global.resultEl,
      resultItem: global.resultItem,
      resultItemHighlighted: global.resultItemHighlighted,
      debounceDelay: 300
    };
    
    export const inputView = {
      inputEl: global.inputEl,
      onSelect: 'onSelect',
      throttleDelay: 60
    };
    
    export const resultView = {
      resultEl: global.resultEl,
      resultItem: global.resultItem,
      resultItemHighlighted: global.resultItemHighlighted,
    	
      // 잦은 변경이 예상되는 템플릿 함수들 
      noResultSuggestionTemplate() {
        //...
      },
    
      noResultRecentQueryTemplate() {
        //...
      },
    
      recentQueryTemplate(recentQueryList) {
    		//...
      },
    	
      suggestionTemplate(query, suggesions) {
    		//....
      },
    	
      // 잦은 변경이 예상되는 데이터 처리함수
      getAutoSuggesionList({ dataSrc, query, config }) {
    		//...
      },
    };
    
    
    
    3 와
    const global = {
      inputEl: '.autoComplete_input',
      resultEl: '.autoComplete_result',
      resultItem: 'autoComplete_result_item',
      resultItemHighlighted: 'autoComplete_result_item-highlighted'
    };
    
    export const model = {
      srcUrl: './src/data.json'
    };
    
    export const controller = {
      inputEl: global.inputEl,
      resultEl: global.resultEl,
      resultItem: global.resultItem,
      resultItemHighlighted: global.resultItemHighlighted,
      debounceDelay: 300
    };
    
    export const inputView = {
      inputEl: global.inputEl,
      onSelect: 'onSelect',
      throttleDelay: 60
    };
    
    export const resultView = {
      resultEl: global.resultEl,
      resultItem: global.resultItem,
      resultItemHighlighted: global.resultItemHighlighted,
    	
      // 잦은 변경이 예상되는 템플릿 함수들 
      noResultSuggestionTemplate() {
        //...
      },
    
      noResultRecentQueryTemplate() {
        //...
      },
    
      recentQueryTemplate(recentQueryList) {
    		//...
      },
    	
      suggestionTemplate(query, suggesions) {
    		//....
      },
    	
      // 잦은 변경이 예상되는 데이터 처리함수
      getAutoSuggesionList({ dataSrc, query, config }) {
    		//...
      },
    };
    
    
    
    4 에 대해 다룬다.
  • 보다 더 나은 이해를 위해서 이 게시물을 참고하면 좋다.

2-1. Debounce(사용자 입력시)

2-1-1. 문제되는 상황

// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }

사용자가 특정 문자열을 입력할 때마다 매번

const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


5 함수가 발동된다. 여기서 매번이 문제가 된다. 사용자는 본래 의도한 특정 문자열을 완성시키기 위해 연속적으로 키보드 입력을 한다. 입력이 연속적으로 들어온다는 것은 사용자가 자신의 의도를 아직 다 표현하지 않았음을 의미한다. 사용자가 의도한 문자열 입력을 완료하지 않은 상태에서 굳이 자동완성 결과를 매번 보여줄 필요가 없으며 이는 리소스 낭비일 수 있다. (아래 화면을 보면 입력시마다
const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


5 함수가 실행되어 자동완성 목록이 업로드 된다.)

React 검색 자동완성 - react geomsaeg jadong-wanseong

2-1-2. Debounce 를 통한 해결

사용자가 입력을 모두 마쳤을 때

const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


5 가 한번만 발동되도록 하고, 연속적인 입력이 들어오면 아직 입력을 마치지 않은것으로 간주하여 함수 실행을 지연시키면 많은 리소스를 절약할 수 있다. 이런 전력을 취해 할 수 있는 UI 제어기법이
const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


3 이다

// debounce.js 
export default (func, delay) => {
  // 이때 inDebounce는 클로저이다.
  let inDebounce; // timeoutID
	// debounce 함수의 실행으로 반환되는 함수
  return (...args) => {
    clearTimeout(inDebounce);
    inDebounce = setTimeout(() => func(...args), delay);
  };
};

// 컨트롤러에서 debounce 사용
class Controller {
  constructor() {
    this.inputEl = document.querySelector(config.inputEl);
    this.resultEl = document.querySelector(config.resultEl);
    // debounce 함수 실행
    this.handelSuggestions = debounce(
      this.handelSuggestions.bind(this),
      config.debounceDelay
    );
    this.attatchEvent();
  }
}

debounce의 로직은 아래와 같다.

  • const global = {
      inputEl: '.autoComplete_input',
      resultEl: '.autoComplete_result',
      resultItem: 'autoComplete_result_item',
      resultItemHighlighted: 'autoComplete_result_item-highlighted'
    };
    
    export const model = {
      srcUrl: './src/data.json'
    };
    
    export const controller = {
      inputEl: global.inputEl,
      resultEl: global.resultEl,
      resultItem: global.resultItem,
      resultItemHighlighted: global.resultItemHighlighted,
      debounceDelay: 300
    };
    
    export const inputView = {
      inputEl: global.inputEl,
      onSelect: 'onSelect',
      throttleDelay: 60
    };
    
    export const resultView = {
      resultEl: global.resultEl,
      resultItem: global.resultItem,
      resultItemHighlighted: global.resultItemHighlighted,
    	
      // 잦은 변경이 예상되는 템플릿 함수들 
      noResultSuggestionTemplate() {
        //...
      },
    
      noResultRecentQueryTemplate() {
        //...
      },
    
      recentQueryTemplate(recentQueryList) {
    		//...
      },
    	
      suggestionTemplate(query, suggesions) {
    		//....
      },
    	
      // 잦은 변경이 예상되는 데이터 처리함수
      getAutoSuggesionList({ dataSrc, query, config }) {
    		//...
      },
    };
    
    
    
    3 함수는
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    0 키워드를 통해
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    1 변수를 선언한다. 그 후
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    2를 받아서 delay 만큼 지연되지 않았을 경우 이전에 등록했던
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    3을 지우고 새로
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    3 을 등록함으로써
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    5 함수의 실행을 지연하는 함수를 반환한다. 반환되는 함수에서
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    1 변수가 사용되는데 이는
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    7 공간에 존재하는
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    8 이다.
  • 반환된 함수가 setTimeout 함수를 실행하기 전에
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    9 함수를 실행하는데 이때 인자로 전달되는
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    1는
    // config.js
    const global = {
    	...
    };
    
    export const model = {
      srcUrl: './src/data.json'
    };
    
    export const controller = {
    	...
    };
    
    export const inputView = {
    	...
    };
    
    export const resultView = {
    	// 템플릿 - 최근 검색 결과 
      recentQueryTemplate(recentQueryList) {
    		...
      },
    	
      // 템플릿 - 자동 완성 결과 
      suggestionTemplate(query, suggesions) {
    		...
      },
    	
      // 데이터 처리 - 모델로 부터 전달받은 데이터에서 자동완성 데이터를 추출하는 함수 
      getAutoSuggesionList({ dataSrc, query, config }) {
    		
      },
    };
    
    1에서
    // config.js
    const global = {
    	...
    };
    
    export const model = {
      srcUrl: './src/data.json'
    };
    
    export const controller = {
    	...
    };
    
    export const inputView = {
    	...
    };
    
    export const resultView = {
    	// 템플릿 - 최근 검색 결과 
      recentQueryTemplate(recentQueryList) {
    		...
      },
    	
      // 템플릿 - 자동 완성 결과 
      suggestionTemplate(query, suggesions) {
    		...
      },
    	
      // 데이터 처리 - 모델로 부터 전달받은 데이터에서 자동완성 데이터를 추출하는 함수 
      getAutoSuggesionList({ dataSrc, query, config }) {
    		
      },
    };
    
    2 으로 반환된
    // config.js
    const global = {
    	...
    };
    
    export const model = {
      srcUrl: './src/data.json'
    };
    
    export const controller = {
    	...
    };
    
    export const inputView = {
    	...
    };
    
    export const resultView = {
    	// 템플릿 - 최근 검색 결과 
      recentQueryTemplate(recentQueryList) {
    		...
      },
    	
      // 템플릿 - 자동 완성 결과 
      suggestionTemplate(query, suggesions) {
    		...
      },
    	
      // 데이터 처리 - 모델로 부터 전달받은 데이터에서 자동완성 데이터를 추출하는 함수 
      getAutoSuggesionList({ dataSrc, query, config }) {
    		
      },
    };
    
    3 이다.
  • // config.js
    const global = {
    	...
    };
    
    export const model = {
      srcUrl: './src/data.json'
    };
    
    export const controller = {
    	...
    };
    
    export const inputView = {
    	...
    };
    
    export const resultView = {
    	// 템플릿 - 최근 검색 결과 
      recentQueryTemplate(recentQueryList) {
    		...
      },
    	
      // 템플릿 - 자동 완성 결과 
      suggestionTemplate(query, suggesions) {
    		...
      },
    	
      // 데이터 처리 - 모델로 부터 전달받은 데이터에서 자동완성 데이터를 추출하는 함수 
      getAutoSuggesionList({ dataSrc, query, config }) {
    		
      },
    };
    
    4이 실행되면 이전에
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    3 을 통해 등록했던
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    5 지연실행이 해제된다. 등록해제 이후 곧 바로 새로운
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    3 실행을 통해 새로운 지연 실행을 예약한다.
  • delay가 300ms 인 경우를 예로 들어 이해해보자.

// 1. keyup 이벤트 발생 
this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));

// 2.doByInputKeyup 실행
doByInputKeyUp(e) {
			// 3. handelSuggestions 실행 
       this.handelSuggestions(e.target.value);
    }
  }

// debounce 를 통해 반환된 함수 내부
(...args) => {
  	// 4. clearTimeout 실행 
    clearTimeout(inDebounce); // 첫번째 실행이므로 inDebounce = undefined, 
  	// 이때 inDebounce는 closure 공간에 존재하는 자유변수(freeVariable) 
  	
  	// 5. handleSuggestion 함수 300ms 지연실행 등록 
  	// 여기서 func 는 handleSuggestion 임
    inDebounce = setTimeout(() => func(...args), delay);
  	// 6. setTimeout 함수가 반환한 timeout Id가 inDebounce 변수에 저장됨
  	//  - 이때 inDebounce 변수는 closer 공간에 존재하는 자유변수(freeVariable)
  	//  - 그로 인해 다음함수가 실행될 때도 이전에 저장했던 inDebounce를 참조하여 사용할 수 있음
  };

// 7. 두번째 keyup 이벤트 발생 (300ms 가 아직 지나지 않은 경우)
this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));

// 8.doByInputKeyup 실행
doByInputKeyUp(e) {
			// 9. handelSuggestions 실행 
       this.handelSuggestions(e.target.value);
    }
  }

// debounce 를 통해 반환된 함수 내부
(...args) => {
  	// 10. clearTimeout 실행 
    clearTimeout(inDebounce); // 5. 에서 등록했던 setTimeout이 해제됨 
  	// 이때 inDebounce는 closure 공간에 존재하는 자유변수(freeVariable) 
  	
  	// 11. handleSuggestion 함수 300ms 지연실행 등록 
  	// 여기서 func 는 handleSuggestion 임
    inDebounce = setTimeout(() => func(...args), delay);
  	// 12. setTimeout 함수가 반환한 timeout Id가 inDebounce 변수에 저장됨
  	//  - 이때 inDebounce 변수는 closer 공간에 존재하는 자유변수(freeVariable)
  	//  - 그로 인해 다음함수가 실행될 때도 이전에 저장했던 inDebounce를 참조하여 사용할 수 있음
  };

// 13. 이벤트 발생 후 300ms 가 흘렀을 때 
// func()에 등록된 handelSuggestions 함수 실행  

디바운스 적용을 한 결과는 아래와 같다. 같이 최종적으로 입력이 완료될 때 까지 자동완성이 보이지 않다가, 모든 입력이 완료된 이후에 한번만 자동완성 결과를 보여준다. 이렇게 하면 매번 callback을 실행하지 않아 리소스를 절약할 수 있다.

2-1-3. debounce 적용 before, after

실제 화면으로 보니 예상대로 잘 동작한다. 짝짝짝!!!

before - 입력시 마다 매번
const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


5 함수 실행

React 검색 자동완성 - react geomsaeg jadong-wanseong

after - 입력이 모두 완료됬을 때 한번만
const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


5 함수 실행

React 검색 자동완성 - react geomsaeg jadong-wanseong

2-1 Throttle (검색결과 네비게이션)

2-1-1. 문제되는 상황

attatchEvent() {
  	// 검색어 입력에 대응하는 keyup 이벤트
    this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
		// 자동완성 결과 네비게이션에 대응하는 keydonw 이벤트
    this.inputEl.addEventListener('keydown', e => this.doByInputKeyDown(e));
}

// ArrowDown, ArrowUp 키 입력시 inputView의 navigate 함수 작동
doByInputKeyDown(e) {
  if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
    e.preventDefault();
    this.inputView.navigate(this.resultEl, e.key);
  }
}

사용자가 자동완성된 결과를 보고 특정 검색어에 도달하기 위해 키보드로 커서(

// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
0)를 제어(
// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
1) 할때, 보통은 해당 자동완성 결과에 도달할 때 까지 키보드를 쭉 누른다.(쭉 누르는 행동을 반영하기 위해서는
// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
2 이벤트가 아닌
// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
3이벤트를 적용해야 한다.
// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
2은 키보드를 눌렀다 땠을 때 발생하기 때문에 연속적인 키보드 반응을 대응하기에 부적합하다.) 구글의 검색창 기능은 이를 반영한 UI를 제공한다.

React 검색 자동완성 - react geomsaeg jadong-wanseong

구글 검색창 UI와 동일한 느낌을 제공하기 위해

// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
3 이벤트를 이용하여
// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
1 함수를 발동시켰다. 사용자가
// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
7를 누르는 동안 연속적으로
// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
1 함수가 발동되는데 함수의 발동속도가 예상보다 무척 빨라 사용자 눈으로 하이라이트 에니메이션을 따라가기가 힘들어 키보드 제어가 어렵다.

React 검색 자동완성 - react geomsaeg jadong-wanseong

2-2-2 throttle 적용으로 문제 해결하기

1초(1000ms)에

// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
3 이벤트가 100번(가상의 예) 작동한다면 100번 중에 16번만
// Controller.js
attatchEvent() {
	this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
}

// keyup 이벤트 실행시 매번 doByInputkeyup 콜백이 실행되고
doByInputKeyUp(e) {
    switch (true) {
      case e.key === 'ArrowDown' || e.key === 'ArrowUp':
        break;

      case e.key === 'Enter':
        this.model.addRecentQuery(e.target.value);
        break;

      default:
        // 검색어 입력인 경우 handleSuggestions 함수가 매번 실행된다.
        this.handelSuggestions(e.target.value);
    }
  }
1 함수가 실행 되게 만들어 사용자가 제어할 수 있는 UI를 구현할 수 있다. 여기서 100번 중에 16번으로 함수의 실행횟수를 제한하는 작업이 바로
const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


4 이다.
const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


4 을 사용하면 사용자에게 제어가능한 UI를 제공할 수 있고 덤으로 함수의 실행을 최소화시켜 리소스도 절약할 수 있다. 이제
const global = {
  inputEl: '.autoComplete_input',
  resultEl: '.autoComplete_result',
  resultItem: 'autoComplete_result_item',
  resultItemHighlighted: 'autoComplete_result_item-highlighted'
};

export const model = {
  srcUrl: './src/data.json'
};

export const controller = {
  inputEl: global.inputEl,
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
  debounceDelay: 300
};

export const inputView = {
  inputEl: global.inputEl,
  onSelect: 'onSelect',
  throttleDelay: 60
};

export const resultView = {
  resultEl: global.resultEl,
  resultItem: global.resultItem,
  resultItemHighlighted: global.resultItemHighlighted,
	
  // 잦은 변경이 예상되는 템플릿 함수들 
  noResultSuggestionTemplate() {
    //...
  },

  noResultRecentQueryTemplate() {
    //...
  },

  recentQueryTemplate(recentQueryList) {
		//...
  },
	
  suggestionTemplate(query, suggesions) {
		//....
  },
	
  // 잦은 변경이 예상되는 데이터 처리함수
  getAutoSuggesionList({ dataSrc, query, config }) {
		//...
  },
};


4 적용을 코드로 살펴보자

// throttle.js 
export default (func, delay) => {
  let inThrottle;
  // throttle 함수의 실행으로 반환되는 함수
  return (...args) => {
    if (!inThrottle) {
      func(...args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
      }, delay);
    }
  };
};


// inputView에서 throttle사용
class InputView {
  constructor() {
    this.inputEl = document.querySelector(config.inputEl);
    // throttle 함수 실행
    this.navigate = throttle(this.navigate.bind(this), config.throttleDelay);
  }
}

throttle 로직은 다음과 같다.

  • const global = {
      inputEl: '.autoComplete_input',
      resultEl: '.autoComplete_result',
      resultItem: 'autoComplete_result_item',
      resultItemHighlighted: 'autoComplete_result_item-highlighted'
    };
    
    export const model = {
      srcUrl: './src/data.json'
    };
    
    export const controller = {
      inputEl: global.inputEl,
      resultEl: global.resultEl,
      resultItem: global.resultItem,
      resultItemHighlighted: global.resultItemHighlighted,
      debounceDelay: 300
    };
    
    export const inputView = {
      inputEl: global.inputEl,
      onSelect: 'onSelect',
      throttleDelay: 60
    };
    
    export const resultView = {
      resultEl: global.resultEl,
      resultItem: global.resultItem,
      resultItemHighlighted: global.resultItemHighlighted,
    	
      // 잦은 변경이 예상되는 템플릿 함수들 
      noResultSuggestionTemplate() {
        //...
      },
    
      noResultRecentQueryTemplate() {
        //...
      },
    
      recentQueryTemplate(recentQueryList) {
    		//...
      },
    	
      suggestionTemplate(query, suggesions) {
    		//....
      },
    	
      // 잦은 변경이 예상되는 데이터 처리함수
      getAutoSuggesionList({ dataSrc, query, config }) {
    		//...
      },
    };
    
    
    
    4 함수는
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    0 키워드로
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    6 변수를 선언한다. 그 후에 인자로 받았던
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    7 만큼 시간이 지나지 않았다면 인자로 받은
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    5 함수를 실행시키지 않는 함수를 반환한다.(
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    7가 지났다면
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    5을 실행시킨다) 반환되는 함수 내부에서
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    6이 사용되는데 이 변수는 클로저 공간의
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    8 이다.
  • 반환된 함수가 실행될 때
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    6 변수의 값을 통해 분기를 나눈다.
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    6 변수의 값이
    // 1. keyup 이벤트 발생 
    this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    
    // 2.doByInputKeyup 실행
    doByInputKeyUp(e) {
    			// 3. handelSuggestions 실행 
           this.handelSuggestions(e.target.value);
        }
      }
    
    // debounce 를 통해 반환된 함수 내부
    (...args) => {
      	// 4. clearTimeout 실행 
        clearTimeout(inDebounce); // 첫번째 실행이므로 inDebounce = undefined, 
      	// 이때 inDebounce는 closure 공간에 존재하는 자유변수(freeVariable) 
      	
      	// 5. handleSuggestion 함수 300ms 지연실행 등록 
      	// 여기서 func 는 handleSuggestion 임
        inDebounce = setTimeout(() => func(...args), delay);
      	// 6. setTimeout 함수가 반환한 timeout Id가 inDebounce 변수에 저장됨
      	//  - 이때 inDebounce 변수는 closer 공간에 존재하는 자유변수(freeVariable)
      	//  - 그로 인해 다음함수가 실행될 때도 이전에 저장했던 inDebounce를 참조하여 사용할 수 있음
      };
    
    // 7. 두번째 keyup 이벤트 발생 (300ms 가 아직 지나지 않은 경우)
    this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    
    // 8.doByInputKeyup 실행
    doByInputKeyUp(e) {
    			// 9. handelSuggestions 실행 
           this.handelSuggestions(e.target.value);
        }
      }
    
    // debounce 를 통해 반환된 함수 내부
    (...args) => {
      	// 10. clearTimeout 실행 
        clearTimeout(inDebounce); // 5. 에서 등록했던 setTimeout이 해제됨 
      	// 이때 inDebounce는 closure 공간에 존재하는 자유변수(freeVariable) 
      	
      	// 11. handleSuggestion 함수 300ms 지연실행 등록 
      	// 여기서 func 는 handleSuggestion 임
        inDebounce = setTimeout(() => func(...args), delay);
      	// 12. setTimeout 함수가 반환한 timeout Id가 inDebounce 변수에 저장됨
      	//  - 이때 inDebounce 변수는 closer 공간에 존재하는 자유변수(freeVariable)
      	//  - 그로 인해 다음함수가 실행될 때도 이전에 저장했던 inDebounce를 참조하여 사용할 수 있음
      };
    
    // 13. 이벤트 발생 후 300ms 가 흘렀을 때 
    // func()에 등록된 handelSuggestions 함수 실행  
    5(처음 실행하는 경우),
    // 1. keyup 이벤트 발생 
    this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    
    // 2.doByInputKeyup 실행
    doByInputKeyUp(e) {
    			// 3. handelSuggestions 실행 
           this.handelSuggestions(e.target.value);
        }
      }
    
    // debounce 를 통해 반환된 함수 내부
    (...args) => {
      	// 4. clearTimeout 실행 
        clearTimeout(inDebounce); // 첫번째 실행이므로 inDebounce = undefined, 
      	// 이때 inDebounce는 closure 공간에 존재하는 자유변수(freeVariable) 
      	
      	// 5. handleSuggestion 함수 300ms 지연실행 등록 
      	// 여기서 func 는 handleSuggestion 임
        inDebounce = setTimeout(() => func(...args), delay);
      	// 6. setTimeout 함수가 반환한 timeout Id가 inDebounce 변수에 저장됨
      	//  - 이때 inDebounce 변수는 closer 공간에 존재하는 자유변수(freeVariable)
      	//  - 그로 인해 다음함수가 실행될 때도 이전에 저장했던 inDebounce를 참조하여 사용할 수 있음
      };
    
    // 7. 두번째 keyup 이벤트 발생 (300ms 가 아직 지나지 않은 경우)
    this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    
    // 8.doByInputKeyup 실행
    doByInputKeyUp(e) {
    			// 9. handelSuggestions 실행 
           this.handelSuggestions(e.target.value);
        }
      }
    
    // debounce 를 통해 반환된 함수 내부
    (...args) => {
      	// 10. clearTimeout 실행 
        clearTimeout(inDebounce); // 5. 에서 등록했던 setTimeout이 해제됨 
      	// 이때 inDebounce는 closure 공간에 존재하는 자유변수(freeVariable) 
      	
      	// 11. handleSuggestion 함수 300ms 지연실행 등록 
      	// 여기서 func 는 handleSuggestion 임
        inDebounce = setTimeout(() => func(...args), delay);
      	// 12. setTimeout 함수가 반환한 timeout Id가 inDebounce 변수에 저장됨
      	//  - 이때 inDebounce 변수는 closer 공간에 존재하는 자유변수(freeVariable)
      	//  - 그로 인해 다음함수가 실행될 때도 이전에 저장했던 inDebounce를 참조하여 사용할 수 있음
      };
    
    // 13. 이벤트 발생 후 300ms 가 흘렀을 때 
    // func()에 등록된 handelSuggestions 함수 실행  
    6(
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    7 시간이 지나서
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    6 변수의 값이
    // 1. keyup 이벤트 발생 
    this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    
    // 2.doByInputKeyup 실행
    doByInputKeyUp(e) {
    			// 3. handelSuggestions 실행 
           this.handelSuggestions(e.target.value);
        }
      }
    
    // debounce 를 통해 반환된 함수 내부
    (...args) => {
      	// 4. clearTimeout 실행 
        clearTimeout(inDebounce); // 첫번째 실행이므로 inDebounce = undefined, 
      	// 이때 inDebounce는 closure 공간에 존재하는 자유변수(freeVariable) 
      	
      	// 5. handleSuggestion 함수 300ms 지연실행 등록 
      	// 여기서 func 는 handleSuggestion 임
        inDebounce = setTimeout(() => func(...args), delay);
      	// 6. setTimeout 함수가 반환한 timeout Id가 inDebounce 변수에 저장됨
      	//  - 이때 inDebounce 변수는 closer 공간에 존재하는 자유변수(freeVariable)
      	//  - 그로 인해 다음함수가 실행될 때도 이전에 저장했던 inDebounce를 참조하여 사용할 수 있음
      };
    
    // 7. 두번째 keyup 이벤트 발생 (300ms 가 아직 지나지 않은 경우)
    this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    
    // 8.doByInputKeyup 실행
    doByInputKeyUp(e) {
    			// 9. handelSuggestions 실행 
           this.handelSuggestions(e.target.value);
        }
      }
    
    // debounce 를 통해 반환된 함수 내부
    (...args) => {
      	// 10. clearTimeout 실행 
        clearTimeout(inDebounce); // 5. 에서 등록했던 setTimeout이 해제됨 
      	// 이때 inDebounce는 closure 공간에 존재하는 자유변수(freeVariable) 
      	
      	// 11. handleSuggestion 함수 300ms 지연실행 등록 
      	// 여기서 func 는 handleSuggestion 임
        inDebounce = setTimeout(() => func(...args), delay);
      	// 12. setTimeout 함수가 반환한 timeout Id가 inDebounce 변수에 저장됨
      	//  - 이때 inDebounce 변수는 closer 공간에 존재하는 자유변수(freeVariable)
      	//  - 그로 인해 다음함수가 실행될 때도 이전에 저장했던 inDebounce를 참조하여 사용할 수 있음
      };
    
    // 13. 이벤트 발생 후 300ms 가 흘렀을 때 
    // func()에 등록된 handelSuggestions 함수 실행  
    6로 바뀐 경우) 일 때 특정 로직이 실행된다.
  • 위 조건을 만족하는 경우
    attatchEvent() {
      	// 검색어 입력에 대응하는 keyup 이벤트
        this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    		// 자동완성 결과 네비게이션에 대응하는 keydonw 이벤트
        this.inputEl.addEventListener('keydown', e => this.doByInputKeyDown(e));
    }
    
    // ArrowDown, ArrowUp 키 입력시 inputView의 navigate 함수 작동
    doByInputKeyDown(e) {
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        e.preventDefault();
        this.inputView.navigate(this.resultEl, e.key);
      }
    }
    0가
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    5 함수에 전달되어 실행되고
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    6 변수에
    attatchEvent() {
      	// 검색어 입력에 대응하는 keyup 이벤트
        this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    		// 자동완성 결과 네비게이션에 대응하는 keydonw 이벤트
        this.inputEl.addEventListener('keydown', e => this.doByInputKeyDown(e));
    }
    
    // ArrowDown, ArrowUp 키 입력시 inputView의 navigate 함수 작동
    doByInputKeyDown(e) {
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        e.preventDefault();
        this.inputView.navigate(this.resultEl, e.key);
      }
    }
    3
    attatchEvent() {
      	// 검색어 입력에 대응하는 keyup 이벤트
        this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    		// 자동완성 결과 네비게이션에 대응하는 keydonw 이벤트
        this.inputEl.addEventListener('keydown', e => this.doByInputKeyDown(e));
    }
    
    // ArrowDown, ArrowUp 키 입력시 inputView의 navigate 함수 작동
    doByInputKeyDown(e) {
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        e.preventDefault();
        this.inputView.navigate(this.resultEl, e.key);
      }
    }
    4 값을 할당한다. 다음으로
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    7 만큼 시간이 지난 후
    // debounce.js 
    export default (func, delay) => {
      // 이때 inDebounce는 클로저이다.
      let inDebounce; // timeoutID
    	// debounce 함수의 실행으로 반환되는 함수
      return (...args) => {
        clearTimeout(inDebounce);
        inDebounce = setTimeout(() => func(...args), delay);
      };
    };
    
    // 컨트롤러에서 debounce 사용
    class Controller {
      constructor() {
        this.inputEl = document.querySelector(config.inputEl);
        this.resultEl = document.querySelector(config.resultEl);
        // debounce 함수 실행
        this.handelSuggestions = debounce(
          this.handelSuggestions.bind(this),
          config.debounceDelay
        );
        this.attatchEvent();
      }
    }
    6변수에
    // 1. keyup 이벤트 발생 
    this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    
    // 2.doByInputKeyup 실행
    doByInputKeyUp(e) {
    			// 3. handelSuggestions 실행 
           this.handelSuggestions(e.target.value);
        }
      }
    
    // debounce 를 통해 반환된 함수 내부
    (...args) => {
      	// 4. clearTimeout 실행 
        clearTimeout(inDebounce); // 첫번째 실행이므로 inDebounce = undefined, 
      	// 이때 inDebounce는 closure 공간에 존재하는 자유변수(freeVariable) 
      	
      	// 5. handleSuggestion 함수 300ms 지연실행 등록 
      	// 여기서 func 는 handleSuggestion 임
        inDebounce = setTimeout(() => func(...args), delay);
      	// 6. setTimeout 함수가 반환한 timeout Id가 inDebounce 변수에 저장됨
      	//  - 이때 inDebounce 변수는 closer 공간에 존재하는 자유변수(freeVariable)
      	//  - 그로 인해 다음함수가 실행될 때도 이전에 저장했던 inDebounce를 참조하여 사용할 수 있음
      };
    
    // 7. 두번째 keyup 이벤트 발생 (300ms 가 아직 지나지 않은 경우)
    this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    
    // 8.doByInputKeyup 실행
    doByInputKeyUp(e) {
    			// 9. handelSuggestions 실행 
           this.handelSuggestions(e.target.value);
        }
      }
    
    // debounce 를 통해 반환된 함수 내부
    (...args) => {
      	// 10. clearTimeout 실행 
        clearTimeout(inDebounce); // 5. 에서 등록했던 setTimeout이 해제됨 
      	// 이때 inDebounce는 closure 공간에 존재하는 자유변수(freeVariable) 
      	
      	// 11. handleSuggestion 함수 300ms 지연실행 등록 
      	// 여기서 func 는 handleSuggestion 임
        inDebounce = setTimeout(() => func(...args), delay);
      	// 12. setTimeout 함수가 반환한 timeout Id가 inDebounce 변수에 저장됨
      	//  - 이때 inDebounce 변수는 closer 공간에 존재하는 자유변수(freeVariable)
      	//  - 그로 인해 다음함수가 실행될 때도 이전에 저장했던 inDebounce를 참조하여 사용할 수 있음
      };
    
    // 13. 이벤트 발생 후 300ms 가 흘렀을 때 
    // func()에 등록된 handelSuggestions 함수 실행  
    6 를 할당하도록
    import { resultView as config } from './config.js';
    
    class ResultView {
      constructor() {
        this.resultEl = document.querySelector(config.resultEl);
      }
    
      renderRecentQuery(dataSrc) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const template =
          dataSrc.length === 0
            ? config.noResultRecentQueryTemplate() //conifg를 여기서 
            : config.recentQueryTemplate(dataSrc);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    
      renderSuggestion(dataSrc, query) {
        this.resultEl.innerHTML = '';
        this.resultEl.style.display = 'block';
        const suggestions = config.getAutoSuggesionList({
          dataSrc,
          query,
          config
        });
        const template =
          suggestions.length === 0
            ? config.noResultSuggestionTemplate()
            : config.suggestionTemplate(query, suggestions);
        this.resultEl.insertAdjacentHTML('afterbegin', template);
      }
    }
    
    export default ResultView;
    3 함수에 callback
    attatchEvent() {
      	// 검색어 입력에 대응하는 keyup 이벤트
        this.inputEl.addEventListener('keyup', e => this.doByInputKeyUp(e));
    		// 자동완성 결과 네비게이션에 대응하는 keydonw 이벤트
        this.inputEl.addEventListener('keydown', e => this.doByInputKeyDown(e));
    }
    
    // ArrowDown, ArrowUp 키 입력시 inputView의 navigate 함수 작동
    doByInputKeyDown(e) {
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        e.preventDefault();
        this.inputView.navigate(this.resultEl, e.key);
      }
    }
    9)을 전달하여 실행시킨다.
  • delay가 60ms 인 경우를 예로 들어 이해해보자.

// 1. keydown 이벤트 발동
attatchEvent() {
		// 자동완성 결과 네비게이션에 대응하는 keydonw 이벤트
    this.inputEl.addEventListener('keydown', e => this.doByInputKeyDown(e));
}

// 2. DoByInputKeyDown 함수 실행 
doByInputKeyDown(e) {
  if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
    // 3. ArrowDown, ArrowUp 키 입력시 inputView의 navigate 함수 작동
    this.inputView.navigate(this.resultEl, e.key);
  }
}

// throttle 함수 실행으로 반환된 함수 내부
  return (...args) => {
// 4. inThrottle 변수의 값이 undefined(처음 실행하는 경우), false(delay 시간이 지나서 inThrottle 변수의 값이 false로 바뀐 경우)만 아래 로직 실행, true(아직 delay만큼 시간이 지나지 않은 경우)에는 callback이 실행되지 않고 종료됨
    if (!inThrottle) {
      // 5.  this.inputView.navigate 함수에 ...args 를 전달하여 실행
      func(...args);
      inThrottle = true;
      // 6.delay 가 지나 setTimeout 함수의 콜백이 실행되기 전까지는 inThrottle은 true 가 되고 
      // 다음번 함수가 실행될 때 (!inThrottle)가 false로 판정되어 this.inputView.navigate가 실행되지 않음
      setTimeout(() => {
        //7. delay 만큼 시간이 지난 후 inThrottle = false; 실행되고 다음번 함수가 실행될 때
        // if(!inThrottle) 가 true 로 판정되어 this.inputView.navigate 실행됨
        inThrottle = false;
      }, delay);
    }
  };

2-2-3 Throttle 적용 before, after

실제 동작을 확인해보니 예상처럼 작동한다 짝짝짝!!

before - 이벤트가 발생하는 모든경우에 콜백함수가 실행되어 사용자가 네비게이션을 제어하기 어려움

React 검색 자동완성 - react geomsaeg jadong-wanseong

after - 이벤트에 발생할 때 특정 시간동안 콜백함수의 실행을 제한하여 사용자가 제어할 수 있는 UI 제공

React 검색 자동완성 - react geomsaeg jadong-wanseong

마치며

변화 대응 전략

변화가 잦은 코드에 대응하기 위한 전략으로 2가지를 생각했다. 2가지 전략을 활용 해봐도 아직 뚜렷한 장단점 혹은 어떤 맥락에서 어떤 전략을 선택해야 할지 파악하지 못했다. 추후 이 내용을 좀 더 발전시켜 내가 사용한 코드에 근거를 마련해야 한다.