이름으로 의도 전달하기
이름만 보고 동작을 예측할 수 있어야 합니다. 주체, 방향, 동사가 일관되면 코드를 읽는 비용이 줄어듭니다.
1. 이름은 동작의 주체와 맥락을 드러나게 짓습니다.
좋지 않은 예시
// 타이머에 의해 자동으로 접힌다는 의도였지만,
// "타이머가 접힌다"로 읽힙니다.
const [isTimerCollapsed, setIsTimerCollapsed] = useState(false);문제
isTimerCollapsed는 "타이머가 접힌 상태"로 읽힙니다. 실제 의도는 "타이머에 의해 자동으로 접힌 상태"인데, 이름의 주체가 달라지면 동작 예측이 완전히 달라집니다.
좋은 예시
// "자동으로 접힌 상태" — 주체가 명확합니다.
const [isAutoCollapsed, setIsAutoCollapsed] = useState(false);2. boolean은 같은 방향으로 맞추어야 합니다.
좋지 않은 예시
const [isAutoCollapsed, setIsAutoCollapsed] = useState(false);
const [isGuideExpanded, setIsGuideExpanded] = useState(false);
// collapsed는 true = 접힘, expanded는 true = 펼쳐짐
const isCollapsed = isAutoCollapsed && !isGuideExpanded;문제
isAutoCollapsed(true=접힘)와 isGuideExpanded(true=펼쳐짐)가 섞이면, 조합할 때마다 방향을 뒤집어야 한다는 것을 한 번 더 생각하게 합니다.
좋은 예시
const [isAutoCollapsed, setIsAutoCollapsed] = useState(false);
const [isManuallyCollapsed, setIsManuallyCollapsed] = useState(true);
// 둘 다 collapsed 기준 — 바로 읽힌다
const isCollapsed = isAutoCollapsed || isManuallyCollapsed;같은 기준으로 맞추면 조건문이 자연어로 읽힙니다.
3. 부정형 이름을 피합니다.
좋지 않은 예시
const [isNotVisible, setIsNotVisible] = useState(true);
// 이중 부정 — "보이지 않는 게 아닐 때"?
if (!isNotVisible) {
return <Banner />;
}
const isDisabled = !hasPermission && !isAdmin;
// "비활성화가 아닐 때" = "활성화될 때"
if (!isDisabled) {
handleSubmit();
}문제
부정형 이름은 조건문에서 이중 부정을 만듭니다. !isNotVisible은 "보이지 않는 게 아닐 때"로 읽히며, 의도를 파악하려면 두 번 생각해야 합니다.
좋은 예시
const [isVisible, setIsVisible] = useState(false);
if (isVisible) {
return <Banner />;
}
const isEnabled = hasPermission || isAdmin;
if (isEnabled) {
handleSubmit();
}긍정형으로 쓰면 조건문이 자연어로 읽힙니다. !isNotVisible보다 isVisible이 의도가 명확합니다.
4. 같은 역할의 함수에는 같은 동사를 씁니다.
좋지 않은 예시
// 전부 API에서 데이터를 가져오는 함수인데, 동사가 제각각
const fetchUserList = async () => { ... };
const getCourseDetail = async () => { ... };
const loadPaymentHistory = async () => { ... };
const retrieveRefundInfo = async () => { ... };문제
동사가 섞이면 "각각 다른 동작인가?"라는 의문이 생깁니다. get은 동기 계산? fetch는 네트워크? load는 캐시? 같은 역할인데 다른 동사를 쓰면 읽는 사람이 불필요한 구분을 시도하게 됩니다.
좋은 예시
const getUserList = async () => { ... };
const getCourseDetail = async () => { ... };
const getPaymentHistory = async () => { ... };
const getRefundInfo = async () => { ... };동사를 통일하면 "같은 종류의 함수"라는 것이 이름만으로 전달됩니다.
5. 두루뭉술한 동사를 피합니다.
좋지 않은 예시
// 무엇을 처리하는지 호출부에서 알 수 없음
const processOrder = (order: Order) => { /* ... */ };
const handleData = (data: unknown) => { /* ... */ };
const manageCart = (cart: Cart) => { /* ... */ };문제
handle, process, manage, update 같은 동사는 "무엇을 어떻게 한다"를 가리지 못합니다. 도메인 어휘가 떠오르지 않을 때 추상 동사로 대체하기 쉽지만, 그럴수록 책임의 윤곽이 흐려집니다. 이름만 보고는 정규화를 하는지, 필터링을 하는지, 저장을 하는지, 또는 그 모두를 하는지 알 수 없습니다.
좋은 예시
// 동사 하나가 동작 하나를 가리킵니다
const calculateOrderTotal = (order: Order) => { /* ... */ };
const parseUserInput = (input: string) => { /* ... */ };
const validateCart = (cart: Cart) => { /* ... */ };handle 은 이벤트 핸들러에만, 나머지는 도메인 어휘로 이름을 짓습니다. 동사가 동작과 일대일로 맞물리면, 이름만 보고도 함수가 무엇을 하는지 예측할 수 있습니다.
6. 이름이 길어지면 책임을 쪼갭니다.
이름이 길어지거나 And/Or 같은 접속사가 들어가는 순간은, 그 함수가 짊어진 책임이 둘 이상이 된 순간입니다. 단어를 줄이지 말고 책임을 줄이면, 이름은 짧아집니다.
좋지 않은 예시
// 이름의 단어 수 = 책임의 수
const getUserDisplayNameWithFallbackAndLogging = (user: User) => {
sendCPLog('user_name_view', { userId: user.id });
if (user.nickname) return user.nickname;
if (user.email) return user.email.split('@')[0];
return '익명';
};
// And 도 같은 신호
const validateAndSubmit = async (form: FormData) => { /* ... */ };
// Or 도 같은 신호 — 호출부의 의도를 안에서 다시 추측합니다
const updateOrCreateUser = (user: User) => { /* ... */ };문제
이름이 displayName, fallback, logging 세 단어로 늘어난 것은 함수가 세 가지 책임을 진다는 자백입니다. 단어 하나를 빼면 그만큼의 동작이 이름에서 가려질 뿐, 동작 자체는 그대로 남습니다. 책임이 늘어날수록 호출부는 매번 "이걸 부르면 어떤 부수 효과가 함께 일어나는가" 를 의심하게 됩니다.
좋은 예시
// 책임 하나당 동사 하나, 이름 하나
const getDisplayName = (user: User) => {
if (user.nickname) return user.nickname;
if (user.email) return user.email.split('@')[0];
return '익명';
};
const handleProfileView = (user: User) => {
sendCPLog('user_name_view', { userId: user.id });
return getDisplayName(user);
};
const validate = (form: FormData) => { /* ... */ };
const submit = async (form: FormData) => { /* ... */ };
const createUser = (user: NewUser) => api.post('/users', user);
const updateUser = (user: User) => api.put(`/users/${user.id}`, user);흐름의 조립은 호출부에서 하고, 각 함수는 한 가지 책임만 가집니다.
7. 값을 반환하는 함수와 효과를 일으키는 함수를 나눕니다.
get*, calculate* 같은 이름은 값만 반환해야 하고, send*, save*, navigate* 같은 이름은 효과만 일으켜야 합니다. 이름은 약속이고, 실제 동작이 이름과 어긋나면 그 약속은 깨집니다.
좋지 않은 예시
// get 인데 안에서 로그·캐시까지 함께 일으킵니다
const getDiscountedPrice = (price: number, userId: string) => {
sendCPLog('price_calculated', { userId });
const cached = cache.get(`price:${userId}`);
if (cached) return cached;
const discounted = price * 0.9;
cache.set(`price:${userId}`, discounted);
return discounted;
};문제
이름은 "값을 가져온다"는 약속인데, 실제로는 로그·캐시 같은 부수 효과가 함께 일어납니다. 이름과 동작이 어긋나면 호출부는 매번 "이걸 호출하면 또 무엇이 일어나는가" 를 의심해야 하고, 테스트도 로깅·캐시를 모킹해야 합니다. 같은 함수를 다른 맥락(예: 미리보기 화면)에서 부르면 의도치 않게 로그가 쌓입니다.
좋은 예시
// 순수 계산은 get 이라는 이름이 정확히 맞습니다
const getDiscountedPrice = (price: number) => price * 0.9;
// 효과는 한 계층 위(훅·핸들러)에서 명시적으로 일으킵니다
const useDiscountedPrice = (price: number, userId: string) => {
return useMemo(() => {
sendCPLog('price_calculated', { userId });
return getDiscountedPrice(price);
}, [price, userId]);
};값과 효과가 분리되면 이름과 동작이 일치하고, 테스트도 순수 함수만 단순하게 확인할 수 있습니다. cf. 숨은 로직 드러내기, 로깅 함수 분리하기.
