-
[JavaScript] 클린 코드 자바스크립트 강의 정리Front-End/JavaScript 2022. 12. 15. 00:04
🍀 목차
JavaScript 클린 코드?
변수 다루기 : 전역 공간 사용 최소화
변수 다루기 : 임시 변수 제거하기
타입 다루기 : 타입 검사, 형변환 주의하기, isNaN
경계 다루기 : 매개변수의 순서가 경계다
분기 다루기 : 값식문
분기 다루기 : 삼항 연산자 다루기, Truthy & FalsyUdemy 클린코드 자바스크립트(강사 장 현석님(Poco Jang), https://www.udemy.com/course/clean-code-js/) 강의를 개인적으로 정리하다 복습 겸 깔끔하게 포스팅하고 싶었다.
나쁜 코드인 이유와 습관을 고쳐주시는 강의였다.
모든 강의 내용 정리가 아니기에 내용이 흥미로우시다면 수강 추천드립니다! 👍
JavaScript 클린 코드?
해당 강의는 클린 코드에 대해 고민하는 것을 목적으로 하고 있다.
클린 코드에 대한 고민이란
- 타인(강사, 레퍼런스)이 정의한 답을 의심하는 것이다. 👉 맹목적으로 믿기 때문(생각을 하지 않음).
- 의심하되, 배움에는 열린 태도를 가지는 것이다. 👉 스스로의 디자인 패턴, 설계 같은 Frame에 갇히기 때문.
- 직접 생각하고 또 고민하는 것이다. 👉 의심하고, 열린 태도로 배우면 된다.
- 흔히 알려진 JavaScript 코드 스타일에 대한 견해를 탐구하는 것이다. 👉 Google, ECMA, Prettier, ESLint 패턴이 생긴 이유를 따라가 보자.
JavaScript는 타입이 동적으로 형변환되는 몽키 패치가 일어난다. 개발자의 의도와는 다른 결과가 나올 수 있는 것.
또한 단기간에 만들어졌고, 여러 언어의 특징들이 섞여 있는 언어이기도 하다. 장점들도 많지만 단점들도 많다.
몽키 패치(Monkey Patch)
안티 패턴. 런타임 중인 프로그램의 내용이 변경되는 행동을 의미한다.현재 JavaScript는 브라우저 외에도 서버, 데스크톱 애플리케이션 등 무한하게 쓰이는 언어이다.
즉, JavaScript 클린 코드는 어디에서나 존재하고 작동하는 JavaScript를 안전하게 작성하기 위한 것이다.
변수 다루기 : 전역 공간 사용 최소화
전역 공간이란 최상위 객체를 말하며 NodeJS 환경에서는
Global
, 브라우저 환경에서는Window
이다.전역 공간의 정의를 알았으니, 이제 사용을 최소화해야 하는 이유를 보자.
위의 코드의 결과는,
쉽게 예측할 수 있다. 그런데 index2.js에서 test 변수를 출력했을 때는 어떻게 될까?
오류가 나지도 않으며,
출력도 된다. test변수가 전역 공간을 사용하고 있기 때문이다.
다시 전역 공간 window를 출력했을 때, var로 선언한 test가 window 객체 안에 존재하는 것을 확인할 수 있다.
현재는 두 개의 js 파일이지만 이러한 파일이 100개, 1000개라면...? 이 상황은 위험한 상황이 될 수도 있다!
이 파일에서만 사용되는 변수가 다른 파일에서 오타로 인해 사용되고 있다면?
혹시라도 그 안에서 재선언, 재할당도 된다면? 값을 예측하기도 힘들어진다.
따라서, "어디서나 접근이 가능한" 전역 공간은 더럽히지 않아야 한다(위험성이 다분).
전역 공간 사용 최소화 방법을 정리해 보자.
1. 전역 변수❌, 지역 변수⭕ : var는 function scope, let, const는 block scope이다.
// 전역공간 { let test="등록 안됨"; } for(let i=0;i<3;i++){ } // i는 등록 안됨. for(var index=0;index<3;index++){ } // 전역 공간에 index가 등록됨. (조건문은 함수가 아님.)
위 코드에서 index는 전역 공간 안에 존재하게 된다. 따라서, 변수의 scope를 잘 생각하며 전역 공간에 등록되지 않도록 주의한다(지역 변수만 만들기).
2. window, global에 접근하며 조작하지 않는다.
window와 global 영역을 계속 조작하면 매우 위험해질 수 있다.
3. const & let을 사용하자.
var를 사용하기보다는 let과 const 사용을 권장한다(재할당 되지 않는다면 const 사용).
4. 즉시 실행 함수(IIFE), Module, Closure로 스코프를 나누자.
즉시실행함수(Immediately Invoked Function Expression : IIFE)
: 정의되지마자 즉시 실행되는 함수. 내부 변수가 전역으로 저장되지 않는다.
//즉시실행함수 (function test(){ })(); function test2(){ }
Module
: 파일 단위로 분리되어 독자적인 모듈 스코프를 갖게 된다. 내부의 변수는 전역 변수로 등록되지 않는다.Closure
: 클로저는 함수가 private 한 변수를 가질 수 있게 한다. 그렇기에 전역 변수를 사용하지 않으면서 클로저 함수 내부의 변수에는 계속 접근할 수 있다.
변수 다루기 : 임시 변수 제거하기
임시 변수는 특정 공간 안에서 전역 변수처럼 활용되는 변수다.
function getElements(){ const result = {}; result.title = document.querySelector(".title"); result.text = document.querySelector(".text"); return result; }
위의 임시 객체도 함수가 커지면 사실상 전역 공간과 다름이 없다. result 같은 임시 변수는 위험한 요소가 될 수 있는 것.
잘게 함수를 쪼갠다면 큰 문제는 없겠지만 커다란 함수 내에서 result같은 임시 변수로 로직을 작성한다면 협업을 할 때도 좋은 코드라고는 말할 수 없을 것이다. 변수가 만들어진 순간부터 getElements(GET)라는 네이밍에 맞지 않게 객체 안의 속성들에게 CRUD가 가능해지기 때문이다.
임시 변수를 제거하는 것은 간단하다. 함수의 목적을 명확히 하고 해당 변수가 정말 필요한 지 생각해 보는 것이다.
위의 getElements 함수의 목적은 querySelector라는 Web API로 DOM을 가져오고 객체에 넣어 반환하는 것이다.
이 과정에서 result라는 임시 변수는 꼭 필요할까? ❌, 아니다.
// 훨씬 목적이 명확한 함수가 되었다. function getElements(){ return { title : document.querySelector(".title"), text : document.querySelector(".text"), }; }
결과적으로 해당 변수의 필요 여부를 생각하며 코드를 작성한다면 임시 변수를 최대한 제거할 수 있을 것이다.
함수에 새로운 기능을 추가할 때도 또 다른 함수로 나누어 함수 당 한 가지 일만 할 수 있게 하는 것이 좋다.
타입 다루기 : 타입 검사, 형변환 주의하기, isNaN타입 검사
JavaScript에서 타입을 검사할 때 보통
typeof
연산자를 사용할 때가 많다. typeof는 피연산자 자료형의 문자열을 반환하는 것이 특징이다.알아야 할 점은 이 typeof 연산자는 만능이 아니라는 것이다.
JavaScript의 타입은 크게 분류하면
Primitive 타입(원시 타입)
,Reference 타입(참조 타입, 객체)
으로 나뉜다. Primitive는 값이 변하지 않는 불변, Reference는 값이 변할 수 있는 가변 타입이다. 값에 무엇이 들어올지 모르기에 typeof가 감별할 수 없는 경우가 발생한다. 예를 들어, Class를 typeof의 피연산자로 줄 경우 'function'이 반환되는 식이다.// 그 외에도, // Wrapper Object로 생성하면 object. const str = new String("hello"); typeof str // 'object' typeof null // null은 Primitive. 그러나 'object', 이는 언어적인 오류이다.
JavaScript는 동적 타입 언어이다. 그렇기에 타입 검사 시 많은 주의를 기울여야 한다.
prototype 객체의 __proto__라는 속성은 부모 prototype 객체를 가리키는 링크를 담고 있다. 따라서 부모 객체도 접근할 수 있다는 것인데, 이를 프로토타입 체인이라 부르며 가장 상위는 Object.prototype이다.
Object.prototype.toString()
메서드를 사용해 객체를 나타내는 문자열을 반환시킬 수 있다. (MDN Object.prototype.toString()) 다만, call이나 apply를 같이 사용해야 한다. 이는 toString()의 동작 방식에 나와있다.1번부터 나오고 있는 this는 우리가 call 또는 apply로 전달할 객체이고, 이를 통해 전달된 객체를 나타내는 문자열을 반환하게 된다.
Object.prototype.toString.call('') ; //'[object String]' // Wrapper Object도 string으로 반환. Object.prototype.toString.call(new String('')) ; //'[object String]'
다만 이도 무적은 아니며, 그럼에도 이를 설명하는 이유는 "JavaScript의 타입 검사는 이렇게 신경 쓸 필요가 있다"는 경각심을 갖기 위해서다.
구글에 "javascript is function", "javascript is array" 등을 검색하면 stackoverflow에 다양한 검사방식을 알아볼 수 있는데, 추천과 작성 시간을 확인하며 참고해 보는 것도 좋다(믿을만한 최신 자료를 확인하는 것이 가장 좋음).
형변환 주의하기
// 아래는 모두 true이다. '1' == 1 1 == true 0 == false
느슨한 비교 시 위의 예시들은 암묵적인 형변환이 일어난다.
암묵적인 형변환은 예측하기 어렵고 불안전하기에 명시적인 형변환을 사용하는 것이 좋다.
// 암묵적인 형변환 11 + ' 문자와 결합' // '11 문자와 결합' !!'문자열' // true !!'' // false // 명시적인 형변환 String(11 + ' 문자와 결합') Boolean('문자열') Boolean('') Number('11') parseInt('9.999', 10); // 9 // 추가로 parseInt의 두번째 인자는 꼭 넣어주는 게 좋다. // 10진수가 기본값이 아님.
isNaN
isNaN(is Not a Number, 숫자가 아니다.)는 바로 읽기 불편하다.
typeof 123 // 'number' isNaN(123) // false
숫자가 아닌 것들을 true로 리턴하기에 숫자를 넣어버리면 false를 리턴하는 것인데, 이는 '들어온 숫자는 숫자가 아니다.'가 되는 것이고 직관적인 읽기를 헷갈리게 만든다.
isNaN(123+'테스트') //true Number.isNaN(123+'테스트') //false
isNaN은 느슨한 검사이며, 위의 123+'테스트'는 '123테스트'라는 string이 되어 true가 리턴된다.
아래의 Number.isNaN()은 보다 엄격한 검사를 적용한다. 들어온 값이 오직 number형이고 또한 NaN인 값만이 true를 반환한다. 그렇기에
Number.isNaN()
사용을 권장한다.
경계 다루기 : 매개변수의 순서가 경계다min-max를 다룰 때는 최솟값 최댓값이 포함되는지, 연속성과 규칙성이 없지만 종점 요소들을 다룰 때는 first-last를 고려하는 등 어떠한 경계를 다룰 때는 명시적인 것이 중요하다.
대개 매개변수의 순서를 잘 지키기만 해도 호출하는 함수의 경계를 파악할 수 있다.
// 1부터 50까지의 수로 랜덤 수를 생성하나? genRandomNumber(1,50); // 언제부터 언제까지의 날짜들을 가져오는 구나? getDates('YYYY-MM-DD','YYYY-MM-DD'); // 1부터 5까지를 다루겠지만 규칙은 없을 수 있겠구나? genShuffleArray(1,5);
위의 예시들은 매개변수가 2개여서 어느 정도는 관계를 추측할 수 있다.
따라서 인자는 두 개를 넘지 않도록 고정하는 것이 좋다. 하지만 인자가 너무 많다면...
1. 객체로 담아 넘긴다.
function someFunc({someArg1, someArg2, someArg3}){ //... }
2. arguments, rest parameter
function someFunc(someArg1, ... , someArg4){ } function foo(...args) { return arguments; } foo(1, 2, 3); // { "0": 1, "1": 2, "2": 3 } function func1(a, b, c) { console.log(arguments[0]); // expected output: 1 console.log(arguments[1]); // expected output: 2 console.log(arguments[2]); // expected output: 3 } func1(1, 2, 3); //https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/arguments
3. 랩핑 하는 함수를 만든다(권장하지 않음).
function someFunc(someArg1, someArg2, someArg3){ } function getFunc(someArg1, someArg2){ someFunc(someArg1, undefined, someArg2); }
분기 다루기 : 값식문프로그래밍 언어도 언어이기에 당연히 문법은 중요한 요소이다. 문법을 지키지 않는 다면
Syntax Error
를 마주칠 수 있다.React의 JSX문법이 Babel을 통해 JavaScript로 변환된 예시들을 보며 값식문에 대해 이해해 보자.
// JSX ReactDOM.render( <div id="msg">Hello World!</div>, mountNode ); // Is transformed to this JS ReactDOM.render( React.createElement('div', {id:'msg'},'Hello World!'), mountNode );
div태그는 첫 번째 인자 'div'로, id는 객체로 전달되는 등의 형태를 확인할 수 있다.
아래의 예시도 확인해 보자.
// JSX : ?? <div id={if(condition} {'msg'}}>Hello World!</div> // Is transformed to this JS // ?? React.createElement('div', {id: if(condition) {'msg'}}, 'Hello World!'); // !! 이건 "식"이다. ReactDOM.render(<div id={condition ? 'msg' :null}>Hello World!</div>, mountNode);
React를 사용하며 id값에 조건문을 넣은 적이 있나? 저런 코드를 작성해 본 적이 있나?
Syntax Error가 나기에 없을 것이다.
결론부터 말하면, if'문'은 값이 될 수 없다.
그와 비교해, 마지막의 삼항 연산자를 사용한 코드는 문제가 없다. 이미 값으로 귀결된 '식'이기 때문이다.
이렇게 문과 식을 잘 구별하고, 어떤 것이 값이 될 수 있는 지를 아는 것은 중요하다(문법이기 때문이다).
<p> {(() => { switch(this.state.color){ case 'red': return '#FF0000'; case 'green': return '#00FF00'; case 'blue': return '#0000FF'; default: return '#FFFFFF'; } }){}} </p>
이것은 switch '문'을 사용했음에도 가능하다. 즉시실행함수로 바로 값을 리턴하기 때문에 문을 내부에서 사용할 수 있다.
<p>{this.state.color || 'white'}</p>
이것도 가능하다. 논리연산자를 사용한 것이어서 문이 아니다.
function ReactComponent(){ return( <tbody> {(() => { const rows = []; for(let i=0;i<objectRows.length;i++){ rows.push(<ObjectRow key={i} data={objectRows[i]} />); } return rows; })()} </tbody> ); }
for'문'이다. 임시변수에 값을 넣어 즉시실행함수로 반환한다. 위의 예시는 고차함수로 개선시킬 수 있다.
function ReactComponent(){ return( <tbody> {objectRows.map((obj, i) => ( <ObjectRow key={i} data={obj} /> ))} </tbody> ); }
map이 return 된 값이 JSX 내부에서 렌더링 된다.
function ReactComponent(){ return( <div> {(() => { if(conditionOne) return <span>One</span>; if(conditionTwo) return <span>Two</span>; else conditionOne; return <span>Three</span>; })()} </div> ); }
위처럼 정신없는 if문도 논리연산자와 삼항연산자를 사용하면 개선시킬 수 있다.
function ReactComponent(){ return( <div> {conditionOne && <span>One</span>} {conditionTwo && <span>Two</span>} {!conditionTwo && <span>Three</span>} </div> ); }
이 개선이 우리의 목적은 아니다.
값을 넣어야 할 자리에 문을 넣고 있는지 구분을 잘하며 개발하자는 것이다.
분기 다루기 : 삼항 연산자 다루기, Truthy & Falsy삼항 연산자 다루기
삼항 연산자도 일관성 있게 잘 세워야 한다.
function example(){ return condition1 ? value1 : condition2 ? value2 : condition3 ? value3 : value4; } function example(){ if(condition1) {return value1;} else if(condition2) {return value2;} else if(condition3) {return value3;} else {return value4;} }
편하다고, 혹은 숏코딩을 위해 삼항 연산자를 일관성 없게 나열하면 오히려 가독성을 해친다.
위의 예시에서도 같은 로직이나 if문(혹은 switch문) 사용이 더 좋아 보인다.
// 가독성이... 😓 const example = condition1 ? a === 0 ? 'zero' : 'positive' : 'negative' ; // 감싸 읽기 편하게 하자. const example = condition1 ? (a === 0 ? 'zero' : 'positive') : 'negative');
결국 내 코드도 남이 읽는다는 것을 명심하고 가독성 좋게 작성하자.
function alertMessage(isAdult){ isAdult ? alert('입장 가능') : alert('입장 불가'); }
alert는 void를 return 한다.
값을 리턴하는 것이 아니기에 위의 예시는 의미 있는 코드가 아니다. 삼항 연산자의 참과 거짓 결과 모두
undefined
이기 때문이다.삼항 연산자
3개의 "피"연산자를 취한다.
조건 ? 참(값, 식만 가능) : 거짓(값, 식만 가능)억지로 숏코딩을 위해 쓸 바에는 if-else를 쓰자.
Truthy & Falsy
- Truthy : 참 같은 값. true로 평가되는 값이다. Falsy로 정의된 값이 아니면 모두 Truthy한 값이다.
// 모두 true로 반환되어 if 블록을 실행한다. if(true) if({}) if([]) if(42) if("0") if("false") if(new Date()) if(-42) if(12n) if(3.14) if(-3.14) if(Infinity) if(-Infinity)
- Falsy : 거짓 같은 값. false로 평가되는 값이다.
// 모두 false를 반환, if블록은 실행되지 않는다. if (false) if (null) if (undefined) if (0) if (-0) if (0n) if (NaN) if ("")
값들의 true, false 반환 여부를 알고, 반환 값으로 판별하는 경우 묶어서 구분하면 깔끔하다.
참고자료