본문 바로가기
개발 공부/Self Study

[MTFFM] 9일차: 이름 겹치지 않게 관리하기

by jetproc 2026. 6. 1.
728x90

저번 8일 차를 끝으로 가독성 파트가 끝나고, 오늘은 예측 가능성 파트의 첫 번째 주제인 '이름 겹치지 않게 관리하기'를 공부했다.

이 주제는 단순히 “이름을 예쁘게 짓자”는 이야기가 아니었다. 예를 들어 다음 코드를 보자.

export const UserList = ({ users }) => {
  return (
    <ul>
      {users.map((users) => (
        <li key={users.id}>{users.name}</li>
      ))}
    </ul>
  );
};


바깥의 'users'map 안의 'users'가 같은 이름을 쓰고 있다.  
자바스크립트는 이걸 에러로 보지 않지만, 코드를 읽는 사람은 여기서 한번 멈춘다.

'users.id'를 보는 순간 이 'users'가 배열인지, 배열 안의 한 명인지 다시 확인하게 된다.  
코드는 실행되는데 읽는 사람의 예측은 깨지는 상황이다.

즉,

한 스코프 안에서 같은 단어가 여러 의미로 읽히기 시작하면, 코드를 신뢰하기 어려워진다.

# Before Code

이번 미션의 코드는 장바구니 상품 수량을 조절하는 컴포넌트였다.

// 🎯 리팩토링 미션
// handleUpdateQuantity 함수 내부의 setItems((items) => ...) 영역에서 상위 items 상태명과 충돌하지 않도록 함수형 업데이트 인자명을 직관적으로 변경하세요 (예: prevItems 등).
// JSX 내부의 items.map((item) => ...) 안쪽 버튼 이벤트 영역에서 items.find((item) => ...) 처럼 이름이 겹치는 현상(섀도잉)을 접두사를 활용해 명확하게 찢어내세요.
// 코드를 다 고친 후, 변수명이 겹치지 않게 되었을 때 동료 개발자가 느끼는 '예측 가능성'의 이점을 머릿속으로 정리해 보세요.

import React, {useState} from 'react';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartManagerProps {
  initialItems: CartItem[];
}

export const CartManager: React.FC<CartManagerProps> = ({initialItems}) => {
  const [items, setItems] = useState<CartItem[]>(initialItems);

  // 🚨 1번 문제의 영역: 매개변수 이름이 상위 상태(items)를 교묘하게 가림 (Shadowing)
  const handleUpdateQuantity = (id: string, quantity: number) => {
    setItems((items) => items.map((item) => (item.id === id ? {...item, quantity} : item)));
  };

  // 🚨 2번 문제의 영역: map 내부의 인자 이름과, 내부 내부의 filter 인자 이름이 다 'item'으로 겹침
  return (
    <div className='p-4 border rounded-lg bg-white max-w-md mx-auto'>
      <h2 className='text-lg font-bold mb-4'>장바구니 관리</h2>

      <div className='space-y-3'>
        {items.map((item) => (
          <div key={item.id} className='p-3 border rounded flex justify-between items-center'>
            <div>
              <p className='font-medium'>{item.name}</p>
              <p className='text-sm text-gray-500'>{item.price}원</p>
            </div>

            <div className='flex items-center gap-2'>
              <button
                onClick={() => {
                  // 특정 조건을 검사하기 위해 굳이 내부에서 또 items를 뒤지는데, 인자 이름을 또 item으로 씀
                  const currentItem = items.find((item) => item.id === item.id); // 🚨 지옥의 item 파티
                  if (item.quantity > 1) {
                    handleUpdateQuantity(item.id, item.quantity - 1);
                  }
                }}
                className='px-2 py-1 bg-gray-100 rounded'
              >
                -
              </button>
              <span className='text-sm font-bold'>{item.quantity}</span>
              <button
                onClick={() => handleUpdateQuantity(item.id, item.quantity + 1)}
                className='px-2 py-1 bg-gray-100 rounded'
              >
                +
              </button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};


내가 가장 먼저 찝찝했던 부분은 당연히 위에서 언급한 대로 'setItems((items) => ...)'였다.

바깥에는 상태값인 'items'가 있고, 안쪽 콜백에도 'items'가 있다.
이름이 겹친다고 항상 버그가 나는 건 아니다.  

하지만 이건 코드가 독자에게 계속 의심을 요구한다고 생각하고 신뢰를 잃는 행위라고 생각한다.

더 큰 문제는 감소 버튼 안쪽에 있었다.

const currentItem = items.find((item) => item.id === item.id);


여기서는 바깥 map의 `item`과 find 콜백의 `item`이 겹친다. 
그리고 find의 조건도 굉장히 이상하다.

item.id === item.id

자기 자신과 자기 자신을 비교하고 있다..?
이렇게 되면 조건이 무조건 true가 되기 때문에, 의도와 달리 배열의 가장 첫 번째 요소를 반환하게 되는 치명적인 버그를 낳는다.
이건 단순히 이름이 보기 안 좋은 수준이 아니라, 이름 충돌이 실제 로직을 흐리게 만든 냄새나는 코드였다.


# 고민 점

처음 고친 방향은 비교적 명확했다. 상태 업데이트 콜백의 인자는 이전 상태라는 의미가 드러나도록 `prevItems`로 바꿨다.

const handleUpdateQuantity = (id: string, quantity: number) => {
  setItems((prevItems) =>
    prevItems.map((item) => (item.id === id ? { ...item, quantity } : item))
  );
};

 

문제는 감소 버튼이었다. 나는 처음에 `find` 안쪽의 item 네이밍targetItem으로 이름만 바꿔서 해결하려고 했다.

const currentItem = items.find((targetItem) => targetItem.id === item.id);

if (currentItem && currentItem.quantity > 1) {
  handleUpdateQuantity(item.id, item.quantity - 1);
}

 

이러면 최소한 바깥의 `item`과 안쪽의 탐색 대상은 구분된다.  
그리고 하단 조건문에서 `find`가 실패할 수 있으니 `currentItem &&`로 방어하는 것도 나쁘지 않다고 생각했다.

그런데 이 코드로 AI에게 피드백을 받고 정말 중요한 사실을 알게 되었다.

애초에 왜 다시 `find`를 하고 있지?

전체 코드를 보면 이미 `items.map((item) => ...)` 안에 들어와 있다.  
지금 화면에 그리고 있는 장바구니 상품은 이미 `item`이라는 변수로 사용할 수 있었다.
그런데 다시 전체 `items`를 뒤져서 `currentItem`을 찾고 있었다.

이름을 잘 바꾸면 코드가 나아지는 경우가 있다.  
하지만 이번에는 이름을 바꾸는 것보다, 불필요한 탐색 자체를 지우는 게 더 좋은 방향이었다.


# After Code

정리한 코드는 이렇게 됐다.

...
  const handleUpdateQuantity = (id: string, quantity: number) => {
    setItems((prevItems) =>
      prevItems.map((item) =>
        item.id === id ? { ...item, quantity } : item
      )
    );
  };
...
  return (
  ...
        {items.map((item) => (
          <div key={item.id} className='p-3 border rounded flex justify-between items-center'>
		...
              <button
                onClick={() => {
                  if (item.quantity > 1) {
                    handleUpdateQuantity(item.id, item.quantity - 1);
                  }
                }}
                className='px-2 py-1 bg-gray-100 rounded'
              >
                -
              </button>
             ...
            </div>
          </div>
        ))}
...


이제 이름의 역할이 꽤 선명해졌다.

1. `items`는 장바구니 상품 목록이다.  
2. `prevItems`는 상태 업데이트 직전의 상품 목록이다.  
3. `item`은 map을 돌며 보고 있는 단일 상품이다.

복수형, 단수형, `prev` 접두사가 각각 다른 정보를 준다.  
그래서 같은 줄을 읽으면서도 “이게 어떤 스코프의 값이지?”를 계속 추적하지 않아도 된다.

감소 버튼도 훨씬 단순해졌다.

if (item.quantity > 1) {
  handleUpdateQuantity(item.id, item.quantity - 1);
}


이미 가지고 있는 `item`을 그대로 쓴다.  
불필요한 `find`가 사라지니 `targetItem`, `currentItem` 같은 추가 이름도 필요 없어졌다.

결국 가장 읽기 좋은 이름은 추가로 붙인 이름이 아니라, 불필요한 이름을 만들지 않는 구조에서 나왔다.


# 배운 점

이번 미션에서 배운 건 네이밍 규칙 하나를 외우는 느낌은 아니었다.

물론 기본 규칙은 중요하다. 오늘 배운 기본 규칙을 정리해 보았다.

1. 배열은 복수형으로 둔다. 예: `items`, `users`
2. 순회 중인 단일 요소는 단수형으로 둔다. 예: `item`, `user`
3. 함수형 업데이트의 이전 상태에는 `prevItems`처럼 역할이 드러나는 접두사를 붙인다.
4. 내부 탐색이 정말 필요하다면 `targetItem`, `selectedItem`, `matchedItem`처럼 맥락을 구체화한다.


하지만 오늘 다시 생각해 보게 된 건 다섯 번째 기준이었다.

이름을 바꾸기 전에, 그 이름이 필요한 코드인지 먼저 본다.

처음에는 `items.find((targetItem) => ...)`처럼 이름을 분리하는 데 집중했다.  
그런데 피드백을 받고 보니, 그 탐색 자체가 필요 없는 코드였다.

불필요한 코드가 사라지면 네이밍 문제 자체가 같이 사라진다.  
이번 예제에서는 `find`를 지운 순간 `targetItem`을 고민할 이유도 없어졌다.


결국 예측 가능성은 “좋은 이름을 많이 붙이는 것”만으로 만들어지지 않는다.  
같은 이름이 여러 의미를 갖지 않게 하고, 애초에 불필요한 이름이 생기지 않도록 구조를 단순하게 만드는 데서 나온다.

이름은 역할을 설명해야 하고, 같은 이름은 같은 의미만 가져야 한다.

코드를 읽는 사람이 변수명을 볼 때마다 스코프를 다시 추적하지 않아도 되는 것.  
그게 예측 가능한 코드의 시작점이라고 생각한다.

728x90