프로그래머스에서 프론트엔드 개발을 위한 자바스크립트 스터디(feat. VanillaJS)에 3월 16일부터 매주 참석하고 있습니다. 이번에 코드리뷰문화 체험과 제대로된 코드리뷰를 받아볼수 있어서 좋았고, 다른수강생분들의 코드를 보며, 여러가지를 배울수 있어서 좋았습니다.
스터디 첫 미션을 받기 전 이번 코드리뷰 스터디의 배경이 될 사전지식을 테스트하는 퀴즈를 풀었고, Zoom에서 퀴즈에 대한 풀이가 있었습니다. 쉽게 풀었던 문제들도 있었지만, 알고 있었지만 설명을 못했던 문제들도 있어서 이번에 새롭게 복습 해보는 계기가 되었습니다.
교육명에서 나와있듯이 깃헙에 서로 푸쉬한 코드들에 대한 리뷰를 멘토분들과 수강생분들이랑 같이하게 되었습니다. 처음에는 제 코드를 보여준다는것이 두렵고 낯설었지만, 한편으로는 이번 스터디를 참석하면서 리뷰문화에 관심이 생겼으며, 재밌는 5주가 같다는 생각을 하게 되었습니다.
1주차 미션은 간단한 컴포넌트를 구현하는 미션이었습니다. 멘토님이 참고해주신 예제를 활용하면 쉽게 구현할 수 있는 미션이었지만 코드리뷰를 진행 하면서 많은 리뷰들을 받았고 다음과 같이 해결을 했습니다.
검증 함수에 대해 여러가지를 검증하는 로직들이 섞여 있어 하나의 함수에 대해 많은 부담이 있다는 내용이었습니다.
this.validate=function(data){
try{
if(!data){//null or undefined (falsy활용)
throw('No Data');
}else if(typeof data !== 'object' || data.length === 0){//배열은 type이 object중 하나.. 배열의 길이를 이용해 배열의 여부를 판단합니다.
throw('Not Correnct Data Type')
}else if(this===window){//new 키워드를 안붙이고 함수 실행시 에러 발생. new 키워드를 이용하면, this는 인스턴스를 의미, 그냥 함수호출시 this는 window를 가리킴.
throw('인스턴스를 생성하지 않았습니다.');
}
this.data=data;
}catch(e){
console.error(e);
return false;
}
return true;
}
validate
라는 함수안에 초기화 확인여부와 데이터 확인 함수들이 한 곳에 있어 복잡하다는 느낌이 들었습니다.
this.validate=function(data){
if( (!this.validateData(data)) || (!this.isCreateInstance()) ){
return false;
}
this.data = data;
return true;
}
this.validateData=function(data){
try{
if(!data){
throw('No Data');
}else if(Array.isArray(data)){
throw('Not Correnct Data Type')
}
}catch(e){
console.error(e);
return false;
}
return true;
}
this.isCreateInstance=function(){
try{
if(this===window){
throw('인스턴스를 생성하지 않았습니다.');
}
}catch(e){
console.error(e);
return false;
}
return true;
}
관심사 분리를 위해 데이터 검증과 초기화확인 관련 함수를 따로 구분했습니다.
이에 대해서는 객체지향 프로그래밍의 법칙인 SOLID 중 하나인 단일책임원칙(Single Responsibility Principle) 을 참고하면 좋을꺼 같다는 내용이 있었습니다.
하나의 객체에는 하나의 동작에대한 책임을 진다
반드시 규칙을 지켜야한다는 내용은 없지만, 함수에도 적용을 해보면 좋을꺼같다는 생각이 들었습니다.
렌더링 과정의 소스 코드에서 아래의 리뷰내용이 있었습니다.
감싸는 태그의 의미를 잘 드러날수있게 작성하고 데이터를 랜더링하는 관심사를 잘 드러날수있게 작성하면 어떨까요?
XXX.innerHTML = list.reduce( (prev,curr)=> `${prev}
<li>
${curr.text}
</li>`
,'<ul>')
`</ul>`;
reduce
에서 첫번쨰는 callback
함수를 두번째는 배열에 진입했을때 초기값을 넣어줍니다. 문법에 맞게 작성 하였으나, 다른사람이 처음봤을 때 헷갈릴수도 있겠다라는 생각이 들었습니다.
XXX.innerHTML=`<ul>
${data.reduce((prev,curr)=>{
return `${prev}<li>${curr.text}</li>`;
}, '')}
</ul>`
변경후 태그의 ul
태그안에 li
목록으로 감싸는 tree구조에 맞게 변경해서 가독성이 좋아진거 같습니다. 사실 성능향상과는 관련 없는 사항이지만, 앞으로 가독성에 맞게 코드를 작성하는것도 고민해야할꺼 같습니다.
"넘겨 받은 데이터를 랜더링 하는 과정에서 값의 흐름이 코드를 자세히 읽어봐야 알수있다" 는 이야기가 나왔습니다.
this.render=function(){
this.$el.innerHTML=`${this.data.reduce((prev,curr)=>{
return `${prev}
<li> ${curr.isCompleted ? `<s>${curr.text}</s>`: curr.text} </li>`;
},'<ul>')}
</ul>`;
}
만약에 this.data
를 어디서 바인딩하는지를 확인하려면 위에 메서드만으로 알 수 없기때문에, 해당 파일 전체를 확인하거나 그래도 알수없다면 다른 스크립트 파일까지도 확인해야할수있는 문제점이 있었습니다.
this.render=function(data){
this.$el.innerHTML=`${data.reduce((prev,curr)=>{
return `${prev}
<li> ${curr.isCompleted ? `<s>${curr.text}</s>`: curr.text} </li>`;
},'<ul>')}
</ul>`;
}
위 처럼 매개변수로 받으면, render를 호출하는 시점만 찾으면 되기때문에 data
의 흐름을 알기 쉬워질꺼같습니다.
다른분들은 어떻게 구현을 했는가를 지켜보면서 기발하다는 생각이 들었고, 본받고 싶은 코드들도 많이 있었습니다. 요구사항에 맞는 오류없는 개발도 중요하지만, 데이터 흐름에 대한 고민, 가독성 좋은 코드 작성과 리팩토링의 방향등 다양한 방면에 대해 조금더 고민을 해야겠다는 생각이 드는 한 주 였습니다.
두번째 미션은 이전 과제를 업그레이드 하는 미션이었습니다. 난이도는 전보다 있었고 구현에 어려움은 없었습니다만, 어떻게하면 효율적으로 구현을 해야하는지 고민을 많이 했습니다.
//A.js
function A(){
//...A 컴포넌트기능
//B컴포넌트 기능 구현 시작
this.b_render=()=>{
BElement.innerHTML='<input type="text">';
this.b_bindEvent();
}
this.b_bindEvent=()=>{
BElement.addEventListener('keyup',e=>{
//이벤트 구현
})
}
}
//App.js
const AComponent = new A();
기존 A 컴포넌트에 B 컴포넌트의 기능을 추가하려고 합니다. 만약에 위에 소스 처럼 추가하게 되면 B컴포넌트까지 간섭을 하여 컴포넌트의 영향이 더 커집니다. 여기서 C,D....Z 기능이 더 많이 추가된다면 A.js
파일은 더 커지면서 유지보수에 어려움이 생길것입니다..
//A.js
function A(){
//...A 컴포넌트기능
}
//B.js
function B(){
//B컴포넌트 기능 구현 시작
this.render=()=>{
BElement.innerHTML='<input type="text">';
this.b_bindEvent();
}
this.bindEvent=()=>{
BElement.addEventListener('submit',e=>{
//이벤트 구현
})
}
}
//App.js
const AComp = new A();
const BComp = new B();
B.js를 추가하여 B 컴포넌트에 대한 render에만 집중하게했습니다. 이제 A파일에는 A컴포넌트의 render에만 집중을 할수있게 됩니다.
그 다음은 B 컴포넌트의 keyup
이벤트에서 값을 받아 A컴포넌트에 전달을 하려면 App.js
에서 이벤트 함수를 구현하여, B 컴포넌트 인스턴스 생성시 해당이벤트를 매개변수로 받아 사용할 수 있게 해야합니다.
//B.js
fuction B(onSubmit){
this.onSubmit = onSubmit;
this.bindEvent=()=>{
BElement.addEventListener('submit',onSubmit);
}
this.bindEvent();
}
///App.js
const onSubmit = (event)=>{
//...service로직 구현
}
const AComp = new A();
const BComp = new B(onSubmit);
예를 들어 입력하는영역과 목록을 보여주는 페이지가 있다고 합시다. 입력한 내용을 버튼을 클릭하여 등록할때 입력하는 영역에 foucs를 잃게 됩니다. 만약 사용자가 목록을 계속입력하려고 마우스를 이용해 input
태그에 focusing을 하는 과정이 반복 된다면, 사용에 불편함을 느낄수도 있을껍니다.
이런 불편함을 해결하기위해 등록이 끝나고 아래와 같은 조치를 통해 바로 입력할 수 있습니다.
this.bindEvent=()=>{
BElement.addEventListener('submit',e=>{
//내용저장로직
e.target.focus();//text에 focusing
e.target.value='';//text에 입력했던값 비워두기
})
}
보통 text에 클릭이벤트를 통해 삭제또는 변경하고자 할때 <a>Blah Blah</a>
이런식으로 하는 경우가 많았습니다. MDN 인라인 텍스트 시멘틱에에서 a 태그의 설명에대한 한글 번역이 아래와같이 나와있습니다.
HTML
a
요소(앵커 요소)는 href 특성을 통해 다른 페이지나 같은 페이지의 어느 위치, 파일, 이메일 주소와 그 외 다른 URL로 연결할 수 있는 하이퍼링크를 만듭니다.
여기서는 페이지를 이동하는것이 아니기 때문에 a
태그의 정의에 맞게 사용되지 사용하지 않고있었던 겁니다. 앞으로는 HTML 작성시에도 이런점에 유의하여 작성을 해야겠습니다.
이전부터 많이 들었던 개념이지만 면접에서 설명을한다면 도무지 자신이 없어서 정리를 했습니다.
<ul>
<li>아이템1</li>
<li>아이템2</li>
<li>아이템3</li>
</ul>
Array.from(document.querySelectorAll('li')).forEach(li=>{
li.addEventListener('click',({target})=>{
console.log(target.innerText);
});
});
각 li클릭 요소에 대한 이벤트정의 입니다. li 요소가 고정일때는 문제가 없지만 li가 동적으로 추가되면 추가된 요소에대해 이벤트를 다시 추가해야하는 단점이 있습니다. 그래서 이를 해결하기 위해 이벤트 Bubbling
속성을 활용해 각 li
의 이벤트내용을 ul
이벤트에 위임하는것입니다.
bubbling
은 네이버 사전에서 공학용어로써 액체 속에 높은 압력으로 가스를 주입하여 기포를 만드는 일
이라고 나와있습니다. 이때 엑체속을 Dom
이고 가스 주입
은 이벤트 추가, 기포를 만드는일
이 버블링이라 가정해보겠습니다. 물안에 가스를 주입하면 기포가 만들어지면서 기포가 수면위로 떠오릅니다. 그때 이벤트는 DOM의 상위 요소인 <body>
까지 전파가 되는거랑 비슷한 상황이라고 생각하시면 됩니다. 이를 정리해보면
Bubbling
은 하위요소에서 이벤트가 발생하면 상위요소로 이벤트가 전달이 되는 현상을 말합니다.
위에 설명을 바탕으로 이벤트를 변경해보겠습니다.
document.querySelector('ul')).addEventListener('click',({target})=>{
const {nodeName} = target;
if(nodeName ==='LI'){
console.log(target.innerText);
}
});
});
소스에서 보듯이 이벤트 버블링의 속성을 이용해 ul
태그에 이벤트를 위임하여, ul
태그의 자식요소에서 클릭이 발생하면 bubbling
을 통해 ul
이벤트리스너에 접근을 하게 되어, 클릭한 li
의 텍스트값을 콘솔에 출력할수 있게됩니다.
localStorage의 값들은 문자열이 들어가는데, JSON string값을 parse하려고 하는데 불러온 값이 일반 문자열 값이면 예외가 발생하기 때문에 try~catch
로 감싸서 처리하는것이 중요.
기본적으로 error메시지나 API호출에 필요한 기본 URL은 상수로 선언해두면 변경시 되면 모든 파일을 뒤져가며 변경할 필요없게 상수로 관리.