제9장의 주제는 변수가 독자에게 주는 부담을 어떻게 줄일 것인가이다. 변수가 많을수록, 스코프(scope)가 넓을수록, 덮어쓰여지는 횟수가 많을수록, 독자는 머릿속으로 기억해야 할 것이 늘어나고 코드가 이해하기 어려워진다.
지금까지의 메모와 감상문 요약↓
가독성에 기여하지 않는 변수는 제거해 버린다. 변수는 많다고 해서 더 알아보기 쉬운 것은 아니며, 불필요한 변수는 그대로 기억의 부담이 된다. 제거할 수 있는 변수는 다음 3가지로 나눌 수 있다.
단 한 번만 할당되고, 단 한 번만 사용되며, 복잡한 식을 분할하는 것도 아니고 의미를 보충하는 것도 아닌 변수는, 식을 그대로 쓰는 것이 더 짧고 간편하다.
나쁜 예 (불필요한 임시 변수 now)
const now = new Date();
user.lastViewTime = now;
개선 후
user.lastViewTime = new Date();
now는 새로운 정보를 아무것도 추가하지 않는다.
중간 결과를 다른 변수에 저장하지 않고, 처리할 수 있는 시점에서 처리하고 빨리 return 하는 것이 좋다. 배열에서 요소를 찾아 삭제하는 장면이 전형적이다.
나쁜 예 (중간 변수 indexToRemove가 있음)
function removeOne(array, targetValue) {
let indexToRemove = null;
for (let i = 0; i < array.length; i++) {
...
개선 후 (찾은 시점에서 삭제하고 return 함)
function removeOne(array, targetValue) {
for (let i = 0; i < array.length; i++) {
if (array[i] === targetValue) {
...
루프의 종료를 제어하기 위한 것뿐인 done나 found 같은 플래그 변수는, 대부분의 경우 break나 return으로 대체될 수 있다.
나쁜 예 (불필요한 제어 변수 found)
function hasValue(array, target) {
let found = false;
for (const item of array) {
...
개선 후
function hasValue(array, target) {
for (const item of array) {
if (item === target) {
...
원칙은 변수가 보이는 줄 수를 가능한 한 적게 만드는 것이다. 스코프가 좁을수록, 독자가 그 변수를 신경 쓰는 범위도 좁아진다.
JavaScript에서는 전역 변수(global variable)가 이름 충돌이나 상태 혼란을 일으키기 쉽다. 어떤 함수만 사용하는 변수라면, 클로저(closure)로 숨겨버린다.
나쁜 예 (전역 변수 submitted가 잘못 덮어쓰여질 우려)
// 글로벌 스코프
let submitted = false;
function submitForm(formName) {
...
개선 후 (즉시 실행 함수로 감싸서 submitted를 내부로 가두기)
const submitForm = (function () {
let submitted = false; // 내부의 함수에서만 보임
return function (formName) {
...
변수를 함수의 맨 앞에 모아서 선언하지 않고, 가장 먼저 사용하기 직전에 선언한다.
나쁜 예 (변수가 맨 앞에 모여서, 독자는 3개의 변수를 동시에 기억하게 됨)
function filterReplies(originalId) {
const filteredReplies = [];
const rootMessage = Messages.findById(originalId);
...
개선 후 (사용 직전에 정의하여, 동시에 기억할 변수 수를 줄임)
function filterReplies(originalId) {
const rootMessage = Messages.findById(originalId);
rootMessage.viewCount += 1;
...
var 대신 let과 const를 사용하고, 변수를 if나 for 같은 최소한의 블록에 가둔다. 다음 예시에서는 elem이 루프 안에서만 보임.
値がころころ変わる変数は追跡しづらい。書き換えられる回数が増えるほど、読み手は現在の値を更新し続けなければならない。
JavaScript ではできるだけ let ではなく const を使い、代入を一度に制限する。段階的な計算でも、1つの変数を書き換えるのではなく、複数の読み取り専用の変数で表せる。
나쁜 예 (하나의 변수를 여러 번 덮어쓰고, 각 단계의 값을 추적해야 함)
let price = basePrice;
price = price * count;
price = price - discount;
...
개선 후 (각 변수는 한 번만 할당되며 의미도 명확함)
const subtotal = basePrice * count;
const afterDiscount = subtotal - discount;
const finalPrice = afterDiscount * taxRate;
어떤 변수도 값이 바뀌지 않기 때문에, 읽을 때 현재 값을 역산할 필요가 없다. 다만, 언제나 한 번의 할당으로 만들 수 있는 것은 아니다. 그럼에도 불구하고, 변수를 덮어쓰는 부분을 가능한 한 줄인다는 방침은 변하지 않는다.
여기까지의 기법들을 모아서 사용하는 예시가 나온다. 페이지 위에 input1, input2, input3 같은 id의 입력란이 있고, 첫 번째 비어 있는 입력란을 찾아 값을 넣고 그 DOM 요소를 반환하는 함수 setFirstEmptyInput을 작성한다. 빈 칸이 없으면 null을 반환한다.
첫 버전은 변수가 많고, 덮어쓰기도 잦다.
var setFirstEmptyInput = function (newValue) {
var i = 1;
var found = false;
...
문제는 세 가지가 있다. found는 불필요한 제어 흐름 변수이고, elem은 루프 외부에서 정의되어 안에서 여러 번 덮어쓰이며, i 역시 루프 밖에 있어 수명이 길다.
먼저 found를 제거하고, 찾은 시점에서 값을 설정하여 반환한다.
var setFirstEmptyInput = function (newValue) {
var i = 1;
var elem = document.getElementById('input' + i);
...
다음으로 for 루프로 변경하여 스코프를 줄인다.
function setFirstEmptyInput(newValue) {
for (let i = 1; true; i++) {
const elem = document.getElementById('input' + i);
...
elem은 루프 안에서 한 번만 할당되며, 보이는 범위도 그 회차에 국한된다. i는 for문 안에 포함되고, found도 사라졌다. 로직이 일직선이 되어 변수의 상태를 머릿속으로 추적할 필요가 없다.
이 장에서 가장 인상 깊었던 것은, 변수에 대한 이야기가 '읽는 사람이 동시에 기억할 수 있는 양에는 한계가 있다'라는 전제에 기반하고 있다는 점이다. 세 가지 절은, 읽는 사람이 신경 쓰지 않아도 되는 것을 줄인다는 같은 원칙을 다른 각도에서 보고 있을 뿐이라고 생각한다.
세 가지 기법 중에서는, 값을 한 번만 쓰는 것이 가장 유용하다고 느꼈다. price를 계속 덮어쓰는 예시는, subtotal, afterDiscount, finalPrice로 바꾸니 변수 이름 자체가 그 단계의 의미를 설명해 준다. 지금까지 const는 실수로 덮어쓰지 않기 위한 보호라고 생각했지만, 진짜 가치는 읽는 사람의 추적 비용을 줄이는 데 있다는 것을 깨달았다.
또한, AI에게 코드를 작성하게 하는 요즘 나 자신에게 이 장은 리뷰 기준으로서 도움이 된다. 요구사항 정의와 설계는 스스로 정하고, 구현은 AI에 맡기고 있기 때문에, 생성된 코드에 불필요한 변수가 없는지, 스코프가 너무 넓은 변수가 없는지, 여러 번 덮어쓰이는 변수가 없는지를 이 세 가지 점에서 빠르게 확인할 수 있다.