2020.06.22 [TIL] 알아보기 쉬운코드 작성법 & 알고리즘 & 재귀함수
우리가 배운 JavaScript 자료형과 연산, 메서드를 이용해 특정 문제에 대해 과정을 정리하고 코드로 옮기는 연습을 해볼 것입니다.
쉽게 말해 알고리즘을 생각해내는 연습입니다.
추가적으로, 알고리즘에서 중요한 개념 중 하나인 재귀에 대해서 간단히 알아봅니다.
Before You Learn
이 단계로 가기 전에 다음의 체크리스트를 통해 여러분의 상태를 파악해보았으면 좋겠습니다. 혹시 각각의 항목에 대해 자신 있게 “할 수 있다”라고 표시하기 어려울 수도 있습니다. 이제는 그저 문제를 풀어내는 것이 다가 아닙니다. 지금부터라도, 결과보다 과정을 중시하여, 체크리스트에 맞게 문제를 풀어내시기 바랍니다. 해당 내용에 대해 자신 있게 체크할 수 있는 여러분이 되길 바랍니다.
- 요구사항이 하나 이상인 문제를 여러 개로 쪼개서 생각할 수 있다.
- 문제를 일상적인 문장으로 표현할 수 있다. (해당 위치에 바르게 주석을 적을 수 있다.)
- 잘게 쪼갠 특정 문제가 이미 배운 JavaScript의 어떤 개념과 연관되는지 생각해낼 수 있다.
Achievement Goals
- 함수를 재귀 호출 하는 방법에 대해 이해할 수 있다.
- 나뿐만이 아닌, 남들도 읽기 쉬운 코드를 작성할 수 있다.
- 알고리즘 문제를 풀기 위해 Before You Learn의 각 항목에 대해 자신 있게 체크할 수 있다.
Lesson - 재귀함수
재귀 - 어떤 함수가 스스로를 호출하는 것.
5!(5 팩토리얼)
= 5*4*3*2*1
=5*(4*3*2*1) = 5*4!
=5*4*3!
먼저 함수 이름을 fac라고 한다면
function fac(n){
if ( n === 1 ) {
return 1 ;
}
return n * fac(n-1);
}
이제 우리가 5를 넣으면 5는 1이 아니기 때문에 5*4 팩토리얼 해서 계속 계산이 되는 것이다.
예를 들어서 4! 를 계산하는 함수를 만든다면
function fac(4){
if ( n === 1 ) {
return 1;
} else // 추가적으로 안 들어가 있길래 일단 내가 넣어보았는데 맞는지 확인이 필요하다.
return n * fac(n-1);
} // 24 4*3*2*1
자기 자신을 호출하고 넘겨지는 인자 값만 바뀌었다. 자기 스스로를 한 번 더 부르는 것을 재귀라고 한다. 그림으로 그려서 이해해야 된다.
알고리즘 : 문제를 어떻게 풀어나가야 하나?
앞서 알고리즘이란 무엇인지에 대해서, 그리고 문제 분해의 중요성에 대해서 설명했습니다.
혹시 아직도 의사 코드를 작성하기 전에 일단 코드부터 쓰고 있지는 않나요? 항상 코딩하기 전에 어떻게 프로그램이 작동하는지 흐름을 파악하시기 바랍니다. 이번 시간에는 좀 더 보편적이고, 현업에서 쓰일 만한 예제를 통해 알고리즘을 생각해 내는 연습을 해보겠습니다.
문자열 바꾸기 문제로 보는 알고리즘
예를 들어 텍스트에서 foo라는 단어를 찾아 전부 다른 단어로 바꿔주는 코드를 작성한다고 가정해봅시다.
다음은 pseudocode의 예제입니다.
- 텍스트를 입력으로 받는다
- foo라는 단어를 찾는다
- 그 단어를 지운다
- 그 자리에 새로운 단어를 넣는다
- 바뀐 내용을 리턴한다
이제 이 pseudocode를 실제로 작성 가능한 JavaScript로 바꾸고자 한다면, 기본적으로 반복문, 조건문에 대한 이해를 해야 합니다. 그다음엔 JavaScript가 할 수 있는 것과 할수 없는 것들에 대한 이해가 있어야 합니다. (이 예제의 경우, String은 Immutable 하다는 특징, 즉 내용을 직접 바꿀 수 없다는 특징을 알고 있어야 합니다.) 이제 알고리즘을 작성할 준비가 되었습니다. 위의 pseudocode에서 구체화할 수 있는 부분을 구체화해봅시다.
-
텍스트를 입력으로 받는다
-
foo라는 단어를 찾는다
-
끝까지 일치하는지 확인해 본 후,
-
foo라는 글자의 index가 -1이 아니면 단어를 찾은 것이다
-
-
while index를 발견하면
-
index를 이용해 foo 바로 앞까지의 텍스트를 얻어내고
-
foo 대신 새로운 단어를 넣는다
-
foo 이후의 텍스트를 넣는다
-
-
endwhile
-
-
바뀐 내용을 리턴한다
이제 이것을 코드로 옮겨봅시다.
function replaceFoo(text) {
// foo라는 글자의 index가 -1이 아니면 단어를 찾은 것이다
while( text.indexOf('foo')!==1 ) {
// index를 발견하면
let index = text.indexOf('foo');
// index를 이용해 foo 바로 앞까지의 텍스트를 얻어내고
let beforeText = text.slice(0, index);
// foo 대신 새로운 단어를 넣는다
let replaceText = 'BAR'; // foo 이후의 텍스트를 넣는다
let afterText = text.slice(index + 3);
// 'foo'는 세 글자이므로 3을 더함
text = beforeText + replaceText + afterText;
}
return text; // 바뀐 내용을 리턴한다
}
이와 같이 자연스럽게 pseudocode는 주석으로 변경될 수 있습니다.
문제 분해
조금 더 나아가, foo를 찾는 부분과, 텍스트를 바꾸는 기능을 분리해봅시다.
replaceFoo(text)
위 함수는 아래 두 함수로 분리할 수 있습니다.
findFoo(text);
replaceFoo(text);
더 나아가 이는 보편적인 함수로 바꿔볼 수도 있습니다.
find(text, searchString);
replace(text, searchString, replaceString);
여기까지 구현할 수 있다면 여러분들은 이제 보편적인 기능을 가진 훌륭한 유틸리티를 만든 것이나 다름없습니다.
알고리즘을 작성하고, 문제를 분해하는 것의 중요성을 좀 더 이해하셨을 거라 생각됩니다.
알아보기 쉬운 코드 작성법
Style Guide와 관련해서 가장 많은 실수는 indentation, spacing, semicolons, and variable naming에 관련된 것들입니다.
이러한 항목들을 특히 신경 써 주시면 실수를 줄일 수 있습니다!
Style Guide로 트집을 잡는 것처럼 느끼실 수도 있지만, 코드를 읽기 좋게 작성하는 것은 매우 중요한 일입니다. 그렇지 않을 경우, 기술적인 인터뷰에서 발생하는 각각의 오류는 치명적이지 않더라도, 그것들이 쌓여서 느리고 비생산적인 인터뷰 결과를 가져올 수 있습니다. Style Guide에 익숙해지는 과정에서 가장 중요한 것은 섬세하고 날카로운 눈으로 자신의 코드를 스스로 검토할 수 있도록 하는 것입니다
코드 가독성이란?
코드의 가독성을 높이려면, 먼저 코드의 목적이 뚜렷하고 자명해야 합니다. 그리고 그 구조가 일관되고 예측 가능해야 합니다.
구조의 일관성이 부족한 코드일수록, 그런 코드를 해석하기 위해서는 더 많은 노력이 필요합니다. 왜냐하면 코드를 읽을 때 우리는 코드의 구조가 그 코드의 실제 뜻과 연관되어 있을 것이라 가정하고 해석하기 때문입니다.
종종 그런 해석이 어려운 코드를 읽는 사람이 당신일 수 있습니다.
면접 자리에서 자신이 만들어낸 어려운 코드를 해석하기 위해 씨름하는 것은 정말 슬픈 일입니다.
해석하기 어려운 코드는 쓰지 마세요!
아래 가이드라인을 잘 따라서 코딩한다면 당신은 해석하기 쉬운 코드를 쓸 수 있게 될 거예요.
Indentation – 들여 쓰기
논리적으로 종속되어 있는 코드를 쓸 때, 종속된 code block는 주인 code block보다 두 칸 들여 쓰기 합니다.
들여 쓰기를 할 때 탭이 아닌 스페이스를 사용하세요. 탭은 안됩니다.
들여 쓰기와 관련된 탭과 스페이스 사이의 논쟁은 프로그래밍 세계에서는 아주 오래된 논쟁입니다.
그렇기에 취향의 차이일 뿐 이것이 맞다 틀리다의 문제는 아닙니다.
그러나, 많은 JavaScript 프로젝트에서 대부분의 프로젝트가 2개의 스페이스를 쓰고 있고, 점차 들여쓰기 논쟁의 승리자가 되었습니다. 들여쓰기 약속은 프로그래밍 언어마다 조금씩 다릅니다. 다수의 오픈소스 프로젝트가 진행 중인 GitHub에서는, Star(일종의 '좋아요')를 받은 프로젝트의 85% 이상의 JavaScript 프로젝트가 스페이스 들여 쓰기를 사용하고 있습니다.여러분이 절대로 피해야 할 단 한 가지가 있다면, 바로 스페이스와 탭을 혼용해서 쓰는 것입니다. 이것은 금기입니다!
새로운 code block를 시작할 때면 이전 code block보다 2칸 더 들여 쓰기 후 코드를 쓰기 시작하세요
Good:
if (condition) {
action(); }
Bad:
if (condition) {
action(); }
Code block의 마지막 줄을 쓸 때 마지막 줄의 시작은, 시작할 때 줄의 시작과 동일한 곳에서 해주세요.
종속된 code block의 시작에 맞추면 안 됩니다.
Good:
if (condition) {
action();
}
Bad:
if (condition) {
action();
}
Code block이 바뀌고 해당 code block에 맞춰 줄을 바꿀 때 2칸 들여 쓰기 규칙을 지켜야 합니다.
규칙을 어긴 나쁜 예는 다양하지만,
그중 한 가지를 아래에 소개합니다.
Bad:
transmogrify({
a: {
b: function(){
}
}});
Naming – 이름 짓기
Variable names – 변수의 이름 - 지금의 나에게는 이름 짓기가 어려운 부분이 느껴진다.
변수의 이름은 한 단어(Descriptive word)로 표현하는 것이 가장 좋습니다.
당신이 다루고 있는 문제의 영역(domain), 핵심을 잘 묘사해주는 단어일수록 좋습니다.
또한, 구조적인 부분보다는 변수가 존재하는 목적을 고려해서 변수의 이름을 지어야 합니다.
Good:
let animals = ['cat', 'dog', 'fish'];
Bad:
let targetInputs = ['cat', 'dog', 'fish'];
Bad:
let array = ['cat', 'dog', 'fish'];
array와 같은 Collections을 값으로 갖는 변수의 이름은 복수 명사가 좋습니다.
Good:
let animals = ['cat', 'dog', 'fish'];
Bad:
let animalList = ['cat', 'dog', 'fish'];
Bad:
let animal = ['cat', 'dog', 'fish'];
Boolean names – Boolean 이름
Boolean에 관한 변수의 이름은 전형적인 형식을 가집니다.
Boolean 값은 참 혹은 거짓이므로, 관련 변수의 이름 앞에 is 혹은 are를 붙입니다.
is와 are은 단수 복수 형태인 상황인 것과 같은 건가?
예: isValid 또는 areAvailable
Good:
let areEqual = true;
Bad:
let pass = true;
Function names – 함수 이름 짓기
함수 관련 변수의 이름을 지을 때는 동사로 시작해야 합니다.
예를 들면 calculateTotal 혹은 listInventory 등 “{verbObject}” 와 같은 형식입니다. 이런 형식을 사용하면, 변수 이름의 의미가 자명해집니다. 또한, code를 빠르게 훑어보는 사람들이 함수의 입력값과 출력 값, 그리고 둘 사이의 변환 과정을 파악하기가 쉬워집니다.
Bad:
let waterBlocks = function() {
// count how many blocks of ater are collected between each tower
}
Good:
let countWaterBlocks = function() {
// do stuff
}
Good:
let counterWaterBlocksBetweenTowers = function() {
// do stuff
}
Capital letters in variable names – 변수 이름에서의 대문자
-
대부분의 사람들은 변수가 포함한 class를 지시하기 위해 변수 이름의 첫 글자를 대문자로 씁니다.
-
몇몇 사람들은 new 키워드를 사용한 함수에 한해서 대문자를 쓰기도 합니다.
-
상수(constant), 즉 프로그램 전체에서 일정한 값을 가지는 변수의 이름을 정할 때는 그 변수의 이름은 전체를 대문자로 씁니다.
See code:
// Example of a capitalized class constructor function name.
function Animal() {
}
// Example of an all-caps constant variable name.
const MAX_ITEMS_IN_QUEUE = 100;
Symbols / punctuation - 기호 / 구두점 찍기
중괄호를 생략하지 마십시오 (문법적으로 생략 가능한 때에도)
Good:
for (key in object) {
alert(key);
}
Bad:
for (key in object)
alert(key);
Quoting - 인용
JavaScript의 문자열을 쓸 때, 그 처음과 끝에는 작은따옴표를 주로 쓰세요. 큰 따옴표 말고요!
작은따옴표 혹은 큰 따옴표를 한 종류만 꾸준하게 사용하세요. 섞어 쓰는 것은 좋지 않습니다.
작은따옴표를 쓰면 HTML을 쉽게 삽입할 수 있습니다.
HTML은 태그 속성에 주위에 큰 따옴표를 붙이기 때문입니다.
Good:
let dog = 'dog';
let cat = 'cat';
Acceptable(허용):
let dog = "dog";
let cat = "cat";
Bad:
let dog = 'dog';
let cat = "cat";
줄 바꿈이 필요한 문자열을 정의할 때는 ` (backquote)를 사용하는 것도 한 방법입니다.
let multilineText = `this is line one
this is line two
this is line three`;
그러면 줄 바꿈이 필요한 문자열이 언제 있는가?
Semicolons – 세미콜론
코드 문장의 끝에는 항상 ; 세미콜론을 쓰세요.
Good:
alert('hi');
Bad:
alert('hi')
if, for, while 구문의 끝에는 세미콜론을 사용하지 않아야 합니다.
Good:
if (condition) {
response();
}
Bad:
if (condition) {
response();
};
함수 표현식, 즉 함수가 일반적인 구문의 끝에 쓰인 경우, 마치 if, for, while 구문의 끝처럼 보일지라도 코드 끝에 세미콜론을 써야 합니다.
Good:
let greet = function () {
alert('hi');
};
Bad:
let greet = function () {
alert('hi');
}
세미콜론을 쓰지 않아도 된다고 주장하는 사람들도 더러 있으며, ESLint Standard Style에서는 세미콜론을 쓸 필요가 없다고 이야기하고 있습니다. 하지만 여전히 많은 프로젝트에서는 세미콜론을 선호합니다.
ESLint Standard Style? -
Operators and keywords - 연산자와 키워드
엄격한 비교 연산자를 사용하세요
== 혹은 != 를 사용하는 경우, 의도치 않게 비교 대상의 type이 변하여 비교 연산이 실행될 수 있습니다.
그러므로 반드시 === 와 !== 를 사용하세요.
Good:
// this comparison evaluates to false, because the number zero is not the same as the empty string.
if (0 === '') {
alert('looks like they\'re equal');
}
Bad:
// This comparison evaluates to true, because after type coercion, zero and the empty string are equal.
if (0 == '') {
alert('looks like they\'re equal');
}
3항 연산자 (x? y : z)
x ? y : z;
x가 참이면 y를, 거짓이면 z를 실행합니다.
3항 연산자는 code를 compact 하게 만듭니다. 그러나 읽기 어렵습니다.
다음 중 어느 것이 읽기 쉽습니까?
Uses ternary:
return (actual === expected)? : 'FAILED ['+ testName + '] Expected "'+expected+'", but+expected+ got '+'"'+actual+'"';
Uses simple if-statement:
if (actual !== expected) {
console.log('FAILED ' + testName + ': Expected ' + expected + ', but got ' + actual);
} else { console.log('passed');
}
아주 짧고 명확한 코드를 쓸 때만 3항 연산자를 사용하세요.
3항 연산자를 사용할 수 있다고 남용하지 마세요.
가끔 다른 자료들을 검색 시? 가나와 뭔가 했는데 이런 뜻이었구나 이렇게 하나 알아갑니다 3항 연산자!
비전공자한테는 그냥 물음표나 다름없는 것.
not 연산자(!)의 사용
일반적으로 not 연산자(!)는 부정하는 대상 코드의 바로 앞에 붙여 사용합니다.
Bad:
if (! isEqual) {
Good:
if (!isEqual) {
switch 구문
switch 구문은 권장하지 않습니다.
switch 구문은 break 구문 누락으로 인해 오류가 발생하기 쉽습니다.
자세한 내용은 이 블로그 글을 읽어보세요. - 보고 나서 따로 정리해서 올려놔야겠다.
짧게 쓰기
코드는 뜻이 분명하고 실행되는 한, 되도록 짧게 쓰세요.
Not as good:
function square(n) {
let squaredN = n * n; return squaredN;
}
Good:
function square(n) {
return n * n;
}
자명한 코드를 쓰는 것이 코드 작성의 제1원칙입니다.
자명한 코드란, 코멘트나 힌트 없이, 다른 누구에게 설명을 부탁하지 않아도, 작성된 코드의 과정과 결과가 이해하기 쉬운 코드입니다.
되도록 부정을 사용하지 마세요.
너무 많은 부정을 사용해서 코드를 작성하게 되면, 명확성이 떨어질 수 있습니다.
A bit confusing to work out:
if(! equalSizes ||! equalValues)
// negative outcome
} else {
// positive outcome
}
More straightforward:
if(equalSizes && equalValues) {
// positive outcome
} else {
// negative outcome
}
Boolean 결괏값을 바로 return 하세요
Boolean 값을 조건문의 결괏값으로 return 하는 대신 바로 return 하세요
Verbose:
if(charSet.size < text.length) {
return false;
} return true;
Concise:
return charSet.size > text.length;
코드 문장과 구문 사이 공간
코드 사이 공간이 얼마나 필요한가
- 줄 바꿈을 최소로 사용해야 합니다. 줄 바꿈을 덜 쓸수록 한 화면에서 더 많은 코드를 볼 수 있습니다.
Good:
function square(n) {
return n * n;
}
function assertEqual(actual, expected, testName) {
// compare actual and expected
}
Bad:
function square(n) {
return n * n;
}
function assertEqual(actual, expected, testName) {
// compare actual and expected
}
-
최대한 간단하게 코드를 작성하십시오. 긴 코드는 읽기 어렵습니다. 읽기 쉬운 코드가 좋은 코드입니다.
- 줄을 바꿔야 할 때도 있고, 줄 바꿈을 자제해야 할 때도 있습니다.
- 판단 기준은 어떻게 작성해야 이해하기 쉬운 가입니다.
padding 및 추가적인 들여 쓰기
일반적으로, 보기 어렵지 않다면 들여 쓰기를 얼마든지 추가해도 좋습니다.
더 명확하게 보이려고 들여쓰기를 사용할 수 있습니다. 그러한 경우, 처음과 끝에 똑같이 들여쓰기를 사용하세요.
Good:
alert('I chose to put no visual padding around this string');
Good:
alert( 'I chose to put visual padding around this string' );
Bad:
alert( 'I only put visual padding on one side of this string');
변수의 이름과 값을, 같은 항목끼리 묶어서 보기 위해서 들여쓰기를 사용하는 것은 추천하지 않습니다. 만약 이러한 목적으로 들여쓰기를 사용한다면 변수 이름을 변경할 때마다 불필요한 들여쓰기를 너무 많이 사용하게 됩니다.
discouraged:
let firstItem = getFirst();
let secondItem = getSecond();
let thirteenthItem = getThirteenth();
콤마(,) 사이에 띄어쓰기
Bad:
assertEqual(Math.pow(3,2),9, 'Math.pow);
Good:
assertEqual(Math.pow(3, 2), 9, 'Math.pow squares properly');
연산자 사이에 띄어쓰기
Bad:
'Failed ['+testName+']'...
Good:
'Failed [' + testName + ']'...
Bad:
if(actual===expected){
// action }else
{ // alternate action
}
Good:
if(actual === expected){
// action }
else {
// alternate action
}
주석
-
주석을 쓰기보다는 명확한 변수 이름과 함수 이름을 쓰는 것이 좋습니다.
-
코드 구조와 이름을 제대로 작성했다면 코드 자체의 "story"(데이터 흐름과 처리과정)를 충분히 전달할 수 있습니다. 주석으로 "story"를 전달하려는 것은 좋지 않은 방법입니다.
-
주석은 파일을 필요 이상으로 길게 만듭니다. 주석은 작동하는 코드가 아니기 때문입니다.
-
주석은 코드를 쓴 이유 즉, 코드의 목적을 설명해야 합니다. 코드가 어떻게 작동하는지를 설명하면 안 됩니다.
-
쓸데없는 주석은 지워주세요. 불필요한, 날짜가 지난, 임시적인 주석들이 쓸데없는 주석에 해당합니다.
JavaScript gotchas - 자바스크립트를 잡아라!
Snake vs. Camel Casing - 뱀 모양 vs 낙타 모양
JavaScript에서는 변수의 이름을 지정할 때 'Camel Casing'으로 지정합니다.
이것은 Ruby 같은 다른 프로그래밍 언어에서 사용하는 'Snake Casing'과 다릅니다.
Good:
let camelCased = 'Used in javascript';
Bad:
let snake_cased = 'Used in other languages';
JavaScript에서 '뱀 모양'을 사용하는 경우는, 상수 이름을 지을 때입니다.
const MAX_ITEMS_IN_QUEUE = 100;