지금까지의 메모와 감상문 정리↓
왼쪽에 찾고자 하는 값, 오른쪽에 비교 기준을 놓는다. length >= 10
이라고 쓰고, 10 <= length처럼 반대로 쓰지 않는다. 반대로 쓰는 요다 표기법(Yoda notation)은 ==를 =로 잘못 적는 실수를 방지하기 위한 것이다. 지금은 lint나 컴파일러가 경고해주므로, 읽기 힘들게까지 지킬 이유는 없다.
// 좋은 예: 왼쪽은 변하는 값, 오른쪽은 기준
if (length >= 10) {}
while (bytesReceived < bytesExpected) {}
...
배열 순서에는 세 가지의 기준이 있다. 긍정형 조건을 먼저 적는다. 단순한 분기를 먼저 적는다. 눈에 띄는 분기를 먼저 적는다. 이 세 가지가 충돌할 때는 스스로 판단한다. 부정형을 먼저 보여야 하는 경우(null 체크나 에러 처리)는 예외이다.
// 나쁜 예: 시작이 부정형이라 읽는 사람이 한 번 다시 생각하게 된다
if (!urlParams.has("expand_all")) {
renderItems(items);
...
삼항 연산자는 단순한 이분법일 때만 사용한다. 목표는 줄 수를 줄이는 것이 아니라, 다른 사람이 이해할 때까지의 시간을 짧게 하는 것이다.
// 좋은 예: 간단하고 한눈에 보인다
const timeStr = hour >= 12 ? "pm" : "am";
// 나쁜 예: 양쪽 분기에 계산이 있어 읽는 사람이 멈칫한다
...
do/while은 조건이 본체 뒤에 있기 때문에, 읽는 사람은 본체를 다 읽을 때까지 언제 반복할지 알 수 없다. 본체가 반드시 한 번 실행되므로, 입력이 비어있을 때도 깨지기 쉽다. 대부분의 do/while은 while로 다시 쓸 수 있으며, 조건이 위에 있는 것이 위에서 아래로 읽는 흐름에 맞는다.
// 나쁜 예: node가 null이어도 본체가 먼저 한 번 움직여 에러가 난다
do {
if (node.name === name) return true;
...
함수의 출구는 하나만으로 하는 것이 좋다는 옛날 규칙에, 이 책은 찬성하지 않는다. 답이 정해진 시점에 반환해 버리면, 그 뒤의 코드는 조건을 하나 줄여서 읽을 수 있다. 뒷정리가 걱정된다면, try/finally 같은 메커니즘에 맡긴다.
function contains(str, substr) {
// 계속할 필요가 없는 케이스를 먼저 정리한다
if (str === null || substr === null) return false;
...
goto 자체가 나쁜 것이 아니라, 여기저기 뛰어다니는 사용법이 문제이다. 최근 언어에서는 거의 쓸 일이 없고, C에서도 쓰임새는 함수 끝에서의 모아둔 뒷정리 정도이다. 그것도 점프가 여러 번 교차하지 않는 형태로 한정한다. JS에는 goto가 없고, 가장 가까운 것은 레이블이 붙은 break와 continue이다.
하지만 생각은 같아서, 점프는 적고 단순할수록 좋다.
중첩(Nesting)은 조금씩 깊어진다. 기존 코드에 조건을 하나 추가할 때마다 들여쓰기가 한 단계 늘어나고, 고치는 사람은 변경된 부분만 보기 때문에 전체가 재검토되지 않는다. 코드를 추가할 때는 한 발 물러서서, 함수 전체를 새로 쓸 생각으로 본다. 방법은 두 가지가 있는데, 일찍 return하거나, 루프 안에서 continue를 사용하는 것이다.
// 나쁜 예: 단계적으로 깊숙이 숨어 들어간다
function handle(userResult, permissionResult, reply) {
if (userResult === "SUCCESS") {
...
루프 안에서도 같은 생각이 적용된다. 각 회차의 처리가 서로 독립적이라면, continue는
// 나쁜 예: 머릿속에서 식을 끝까지 추적하지 않으면, 무엇을 비교하는지 알 수 없다
if (line.split(':')[0].trim() === "root") {}
// 좋은 예: 변수명으로, 이 부분이 사용자 이름이라는 것을 알 수 있다
...
설명 변수에 비슷하지만 목적이 다르다. 식은 복잡하지 않아도 길고 여러 번 반복된다면, 하나의 개념으로 변수에 묶어버린다.
// 나쁜 예: 매번 이 비교를 다시 읽어야 한다
if (request.user.id === document.ownerId) {
// 사용자는 이 문서를 편집할 수 있다
...
!(a || b || c)
은 !a && !b && !c
와 같고, !(a && b && c)
는 !a || !b || !c
와 같다. 전체에 걸린 부정을 안쪽으로 넣으면, 조건이 순순히 읽힐 때가 많다. 다만, 고쳐서 읽기 쉬워질 경우에만 사용한다.
// 나쁜 예: 전체가 부정되어 있고, 머릿속에서 반전시켜야 한다
if (!(fileExists && !isProtected)) {
error("죄송합니다. 파일을 읽을 수 없습니다");
...
&&
이나 ||
은 단축 평가(short-circuit evaluation)되므로, 이것을 if문의 대용으로 삼아 처리를 한 줄에 채워 넣는 사람이 있다. if로 써야 할 곳은 if로 쓴다.
// 나쁜 예: &&를 if의 대신 사용하고, 부작용이 식 안에 숨겨져 있다
user && user.isAdmin && grantAccess(user);
// 좋은 예
...
보충 설명으로, 이것은 책에 쓰여 있는 내용은 아니고 내가 추가한 이야기지만, JS의 ||는 0나 공백 문자도 "없다"고 간주한다. 기본값을 정할 때는 ??가 더 정확한 경우가 많다.
두 범위(반개 구간 [begin, end))가 겹치는지 판정한다. 처음에는 겹치는 패턴을 전부 적고 싶어지지만, 길어져서 틀리기 쉽다. 보는 각도를 바꾸면, 겹치지 않는 경우는 두 가지 경우뿐이다.
// 나쁜 예: 겹치는 패턴을 전부 나열하고 있다
overlapsWith(other) {
return (
...
비슷한 거대한 식이 여러 번 나올 때는, 중복되는 부분을 변수로 꺼낸다.
// 나쁜 예: 거의 같은 긴 식이 여러 번 쓰여져 있다
const updateHighlight = (messageNum) => {
if ($("#vote_value" + messageNum).html() === "Up") {
...
같은 작성 방식의 반복은, 그것만으로도 복잡함이 된다. 책에서는 C++의 매크로를 사용해, 반복 패턴을 한 곳에 모아두고 있다. JS에는 매크로가 없으므로, 여기서는 루프로 대체했다. 다만, 원래 코드보다 읽기 어려워지는 의미는 없다.
// 나쁜 예: 같은 패턴의 반복으로, 어딘가 한 줄을 잘못 쓰기 쉽다
function addStats(from: Stats, to: Stats) {
to.totalMemory = from.totalMemory + to.totalMemory;
...
이 2장에서는, 감상을 나눠서 쓸 정도의 차이가 없었다. 둘 다, 읽는 사람이 머릿속에서 동시에 기억하고 있어야 하는 것, 즉 지금 어느 분기에 있고, 어떤 조건이 성립되어 있고, 어디까지 실행되었는지 줄이는 이야기이기 때문이다.
6장과 달리, 이 2장의 규칙은 기계적인 경우가 많다. ESLint에도 yoda, no-nested-ternary, max-depth, no-else-return 같은 대응하는 규칙이 있다. AI에게 코드를 쓰게 할 때도, Skill이나 lint로 AI가 지키도록 하면 된다.
예외는 8.1과 8.2의 변수명이라고 생각한다. userOwnsDocument = request.user.id === document.ownerId에서는, 이름이 의도이고 식이 구현에 해당한다. 두 가지가 어긋나 있으면 알아차릴 수 있는 것은, 6.6의 주석과 같은 메커니즘이다. 이름과 식을 같은 AI가 쓰면, 이 체크는 또 작동하지 않게 된다. 규칙은 AI에게 맡길 수 있지만, 그 변수가 업무상 무엇을 의미하는지는 스스로 정하고 싶다.