프로그래머스 과제형 테스트 - 프로그래밍 언어 검색

Gomi·2022년 6월 6일
1

1. 과제란에 공개된 데브매칭 문제


 ‘프로그래머스 과제형 테스트 연습란’의 프로그래밍 언어 검색 문제이다. 원래 프로그래머스 데브매칭 프로그램에서 출제된 문제이지만, 데브매칭은 후에 연습문제의 형태로 답안과 함께 문제를 공개해준다.

 문제는 3시간 안에 검색어 자동 완성을 구현하는 문제인데, 답안을 봤다면 알 수 있지만, 이런 류의 문제는 기업에서 코드의 퀄리티도 중요하게 평가하기 때문에 다른 프레임워크의 개발 방법론을 퓨어 자바스크립트로 구현하려는 노력이 많이 필요할 것이다. 가장 간단하게(?) 푸는 방법 중 하나로 하나의 script.js 파일 안에 모든 변수와 코드를 쑤셔 박는 방법이 있다. React.js외에 바닐라 스크립트로 컴포넌트 구성을 해보려 한 적이 없는 필자는 실전에서 이 방법을 남발했으나, 돌이켜보면 내가 지시사항을 모두 기깔나게 구현했을지라도 합격점을 받을 수는 없었을 것 같다.ㅋㅋ


 프로그래머스는 해당 문제에 관해 자세한 해설을 제공하고 있다.


 해설에 따르면 이 문제에서 요구하는 수준은 '바닐라 스크립트로 재사용성을 고려한 컴포넌트 구성하기'인 것 같다. 바닐라 스크립트로 구현하는 컴포넌트형 SPA는 여러 프레임워크에 익숙해져있을수록 반드시 경험해봐야하는 일로 여겨지는 것 같은데, 인사이트를 넓히는데 많은 도움이 되었다.




2. 컴포넌트 어떻게 만들까


 상태관리를 어떻게 할까 생각해보면 크게 프로토타입을 통한 객체생성이나 클로저 같은 것 들이 떠오른다.

 문제의 해설 코드에서는 프로토타입(new 연산자)를 사용해 객체를 조합하고, 상태가 변할 때 마다 인스턴스의 render함수를 실행하는 구조를 볼 수 있는데, 리액트 같은 라이브러리에서 숱하게 보아온 구조가 내부에서 어떻게 이뤄지는지 대략적으로 알 수 있었다. 객체 내부의 상태관리는 전체적으로 내부의 this바인딩으로 이뤄지고 있었다. 리액트는 이 this바인딩을 함수의 클로저를 이용해서 로직을 분리했다(useState)라고 이해하고 있으나, 어렵기도 하고 일단 객체 내 멤버로 해결하였다.

 개인적으로 프로토타입 객체를 생성할때는 함수를 쓰는 것 보다 역시 전문용어로 syntax sugar라 불리는 클래스 문법으로 작성하는게 편리해서 클래스로 작성했다.

 과제 진행하며 알게 된 점은 클래스 문법 내에서 메소드를 화살표 함수로 선언하면 자유롭게 클래스 멤버 변수에 접근할 수 있다는 것이다. 만약 function키워드로 작성했다면 아래 app.js코드의 this.searchInput인스턴스에서 setApiData메소드를 사용하려할 때 오류가 발생한다. function키워드에서는 함수가 호출될 때의 블럭 스코프를 확인하는데, 외부 클래스에서 setApiData가 호출될 때는 this.suggestion변수가 정의되지 않았기 때문이다.



3. app.js



import SelectedLanguages from './SelectedLanguages.js';
import SearchInput from './SearchInput.js';
import Suggestion from './Suggestion.js';

class App{
    root = document.querySelector('.App');

    constructor(){
      
		//컴토넌트 로딩
        this.selectedLanguages = new SelectedLanguages(this.root);
        this.searchInput = new SearchInput(this.root, this.setApiData);
        this.suggestion = new Suggestion(this.root, this.setLanguage);

        window.addEventListener('keydown',(e)=>{
            if(e.key==='ArrowDown'){
                this.setKey(this.suggestion.key+=1);
            }
            else if(e.key==='ArrowUp'){
                this.setKey(this.suggestion.key-=1);
            }
            else if(e.key==='Enter'){
                this.setLanguage(this.suggestion.key);
            }
        })

    }

    // 화살표함수를 통해 클래스 내부의 this(멤버변수)에 접근할 수 있다.
    // 화살표가 아니면 오류가 발생한다.

	// key입력시 커서 변경하여 렌더링
    setKey=(key)=>{
        const realkey = key<0?4:key%5;
        this.suggestion.key = realkey;
        this.searchInput.input.value =this.suggestion.apiData[realkey]
        this.suggestion.render();
    }
    
	// this.searchInput 컴포넌트에 전달할 setState함수. 
    // input change가 발생할 때마다 api데이터를 요청한다.
    setApiData=(data)=>{
        this.suggestion.apiData = data;
        this.suggestion.render();
    }
    
    // this.suggestion 컴포넌트에 전달할 setState함수.
    // 검색창에 나타난 데이터를 골라 selectedLanguages에 저장한 후 리렌더링한다.
    setLanguage=(key)=>{
        window.alert(this.suggestion.apiData[key]);
        if(this.selectedLanguages.languages.length>=5){
            this.selectedLanguages.languages.splice(0,1);
        }
        this.selectedLanguages.languages.push(this.suggestion.apiData[key]);
        this.selectedLanguages.render();
    }
    
};

const app = new App();

 앱이 실행되면 root dom의 스크립트에 의해 app.js가 실행된다. new App()이 실행되면서 constructor의 다음 코드를 통해 통해 다른 컴포넌트들이 로딩된다. 각 컴포넌트의 constructor에는 render함수가 기본적으로 실행된다.
  this.selectedLanguages = new SelectedLanguages(this.root);
  this.searchInput = new SearchInput(this.root, this.setApiData);
  this.suggestion = new Suggestion(this.root, this.setLanguage);




4 SelectedLanguages.js


export default class SelectedLanguage{

    constructor(target){
        this.target = target;
        this.div = document.createElement('div');
        this.div.className = "SelectedLanguage";
        this.languages = [];
        this.target.appendChild(this.div);

    }

    render(){
        this.div.innerHTML = `
            <ul>
                ${this.languages.map(datum=>`<li>${datum}</li>`).join("")}
            </ul>
        `
    }

}

 검색창 상단부에서 멤버 변수의 배열 데이터를 그냥 렌더링 해주는 컴포넌트이다.




5. SearchInput.js

export default class SearchInput{
    constructor(target, setApiData){
        this.target = target;
        this.form = document.createElement('form');
        this.form.className = "SearchInput";
        this.render();
        this.input = this.form.firstElementChild;
        this.setApiData = setApiData;
      
        // input 이벤트 발생 시 api 요청 및 상태 저장
        this.input.addEventListener('input',(e)=>{
            this.dataFetch(e.target.value).then(data=>{
                this.setApiData(data);
            });
        });
        this.form.addEventListener("submit",(e)=>e.preventDefault());
        this.target.appendChild(this.form);
    }

    render(){
        this.form.innerHTML = `
            <input class="SearchInput__input" type="text"
				placeholder="프로그램 언어를 입력하세요." value="Script"/>
        `;
    }

    async dataFetch(str){
        const data = await fetch(`프로그래머스 api url+${str}`)
        .then(data=>data.json())
        .catch(e=>[]);
        return data;
    }
}

  검색창 컴포넌트이다. input 이벤트 발생 시 api를 요청하고 상위 컴포넌트(app)에 api데이터를 전달한다.

전달 된 데이터는 app.js의 다음 코드를 통해 suggestion 컴포넌트의 리렌더링을 발생시킨다.

this.suggestion.apiData = data;
this.suggestion.render();




6. Suggestion.js


export default class Suggestion{


    constructor(target, setLanguage){
        this.target = target;
        this.div = document.createElement('div');
        this.div.className = "Suggestion";
        this.target.appendChild(this.div);
        this.apiData = [];
        this.key = 0;
        this.setLanguage = setLanguage;
        this.render();
    }

    render(){
        if(this.apiData.length){
            this.div.hidden = false;
            this.div.innerHTML =`
                	<ul>
                    ${this.apiData.map((datum,index)=>index===this.key?
                        `<li class ="Suggestion__item--selected" value=${index}>
							${datum}
							<span class="Suggestion__item--matched">
                            	Script
                            </span>
						 </li>`
                        :
                        `<li value=${index}>
                            ${datum}<span class="Suggestion__item--matched">
                            	Script
                            </span>
                         </li>`).join("")}
                	</ul>
            	`;
            this.div.firstElementChild.childNodes.forEach((node, index)=>{
                node.addEventListener('click',(e)=>{
                    this.setLanguage(index-1);
                });
            })
        }else{
            this.div.hidden = true;
        }
    }
}

 fetch된 api데이터 리스트를 검색창 하단에 나열해서 보여주는 컴포넌트이다.





문제에서 요구한 필수 사항은 다음 코드로 모두 구현했다. (아마도....) 사실 이 모든건 기본사항이고 추가 구현사항이 진짜배기라고 생각하면 조금 암울해지기는 한다 ㅠㅠ

 추가 구현사항에는 API캐싱, 디바운싱, 스토리지를 통한 새로고침에도 현재상태 유지시키기, 검색어와 일치하는 부분 강조 등등이 있다.

어차피 알아두면 좋으니 미래의 나가 꼭 해볼 것이라 믿는다.(?)

profile
터키어 배운 롤 덕후

0개의 댓글