광고
~ / posts / developer
smith@lab:~/posts$ cat developer/operator-precedence-pitfalls-parentheses-lint-guide.md

연산자 우선순위 헷갈리는 조합, 언어별 함정표와 괄호·린터 규칙

비트 마스크를 확인하는 if (flags & MASK == MASK) 가 늘 거짓이 되거나, 중간값을 구하는 lo + (hi - lo) >> 1 이 엉뚱한 값을 내서 한참 헤맨 적 있으시죠. 원인은 대부분 연산자 우선순위예요. 결론부터 말하면 사칙연산끼리는 괄호를 생략해도 되지만, 비트·시프트 연산이 산술이나 비교와 섞일 때, 비교가 두 번 이어질 때, 단항 연산자가 거듭제곱 앞에 올 때, ??·orelse 같은 대체값 연산자가 끼어 있을 때는 괄호를 넣는 게 맞아요. 이 네 경우는 언어마다 규칙이 달라서 같은 식이 다른 값이 되거든요.

2026년 10월에는 Zig 가 이 문제를 언어 차원에서 막았어요. 헷갈리는 조합은 괄호가 없으면 아예 컴파일되지 않게 바꾼 거예요. 이 글은 그 소식을 계기로 "어떤 조합이 왜 위험한지, 내 언어에서는 어떻게 동작하는지, 사람 대신 무엇이 잡아 주는지"를 정리했어요. 아래 결과는 Apple clang 17, Node.js 25.3, 파이썬 3.14, 스위프트(Xcode 기본 툴체인), javac 24 로 직접 돌려 확인했고, Go·러스트는 언어 명세의 우선순위 표로 계산했어요.

광고

연산자 우선순위 버그가 나는 다섯 가지 조합

우선순위 표 전체를 외울 필요는 없어요. 실제 버그는 거의 다 아래 다섯 조합에서 나와요. 각 칸은 해당 언어가 실제로 낸 값이에요(x = 6).

식C·자바스크립트파이썬러스트Go스위프트의도
x & 4 == 40Truetruetruetrue참
1 << 2 + 3323232777
6 - 1 << 220202022사람마다 다름
5 < 4 < 31 / trueFalse컴파일 오류컴파일 오류컴파일 오류거짓
5 + 6 & 333377사람마다 다름

같은 식이 언어를 바꾸면 다른 값이 돼요. C 코드를 Go 로, 파이썬 예제를 자바스크립트로 옮길 때 괄호 없는 비트 연산은 결과가 바뀔 수 있어요. "사람마다 다름"인 줄은 작성자의 뜻을 코드만으로 알 수 없다는 뜻이라, 그 자체로 괄호가 필요한 식이에요.

자바는 우선순위가 C 와 같지만 x & 4 == 4 에서 bad operand types for binary operator '&' 타입 오류를 내서 버그가 컴파일 단계에서 막혀요.

비트 연산 결과를 눈으로 확인하고 싶으면 비트 연산 계산기에 값을 넣어 AND·시프트를 따로 계산해 보면 어느 쪽이 맞는지 바로 보여요.

C 에서 & 가 == 보다 늦게 묶이는 이유

C 의 이 규칙은 설계 실수에 가까워요. C 를 만든 데니스 리치가 직접 남긴 글 The Development of the C Language에 사정이 적혀 있어요. C 의 전신인 B 언어에는 && 가 없어서, if 안의 & 가 논리 AND 처럼 동작했어요. if (a==b & c) 처럼 쓰려면 & 가 == 보다 늦게 묶여야 했죠.

C 에서 && 를 새로 만들면서 기존 코드의 & 를 && 로 바꾸기 쉽게 하려고 & 의 위치를 그대로 두었어요. 리치는 같은 글에서 지금 보면 & 와 == 의 순서를 바꾸는 편이 나았을 거라고 적고, 그 결과 if ((a&mask) == b) 의 안쪽 괄호가 꼭 필요하지만 잊기 쉬워졌다고 썼어요.

이 규칙이 C++, 자바, 자바스크립트, C#, PHP 로 그대로 이어졌어요. MDN 연산자 우선순위 표를 보면 자바스크립트도 동등 비교(8단계)가 &(7)·^(6)·|(5)보다 위에 있어요. 반면 파이썬 언어 레퍼런스는 비교 연산자 전체를 산술·시프트·비트 연산보다 낮게 두었고, 러스트 레퍼런스도 &, ^, | 를 비교보다 위에 둬요. 나중에 나온 언어들이 C 의 이 부분을 일부러 고친 거예요.

언어별 우선순위 단계 수 비교

산술·시프트·비트·비교·논리 이항 연산자만 놓고 단계가 몇 개인지 세 봤어요. 단계가 많을수록 외울 관계가 많고, 위 표의 함정도 많아져요.

이항 연산자 우선순위 단계 수 (산술·시프트·비트·비교·논리만)
자바스크립트자바스크립트 · 11단계11단계CC · 10단계10단계파이썬파이썬 · 10단계10단계러스트러스트 · 9단계9단계스위프트스위프트 · 6단계6단계GoGo · 5단계5단계

자바스크립트는 C 의 10단계에 ** 한 단계가 더해지고, 파이썬은 ** 가 있는 대신 비교를 한 단계로 합쳤어요

단위: 단계 · 자료: MDN·cppreference·파이썬·러스트 레퍼런스·Go 명세·스위프트 기본 precedencegroup 에서 직접 셈 (대입·삼항·범위·형변환 제외)
표로 보기
구분단계 수
자바스크립트11
C10
파이썬10
러스트9
스위프트6
Go5

Go 언어 명세는 이항 연산자를 다섯 단계로만 나눠요. 곱셈류에 * / % << >> & &^ 가, 덧셈류에 + - | ^ 가 들어가고, 그 아래가 비교, &&, || 예요. 비트 연산을 산술과 같은 줄에 넣어 버려서 "비트가 비교보다 늦다"는 함정 자체가 없어요. 스위프트도 비슷하게 & 를 곱셈 그룹에, |·^ 를 덧셈 그룹에 두고 시프트는 그보다 더 위에 둬요.

다만 Go 의 a + b & c 는 a + (b & c) 라서, C 에 익숙한 사람에게는 여전히 함정이에요. 언어를 섞어 쓰는 팀이라면 비트와 산술이 섞이는 곳에 괄호를 넣는 규칙이 가장 안전해요.

직접 돌려 본 결과, 의도한 값과 실제 값

실무 코드에서 자주 보이는 식 다섯 개를 C 로 컴파일해 돌렸어요. 식은 그대로 두고 괄호만 의도대로 넣은 버전과 비교했어요.

괄호를 빠뜨린 C 식의 실제 값 vs 의도한 값
의도한 값실제 값
010203040시프트와 덧셈 · 의도한 값 7값시프트와 덧셈 · 실제 값 32값시프트와 덧셈중간값 시프트 · 의도한 값 15값중간값 시프트 · 실제 값 10값중간값 시프트마스크 후 시프트 · 의도한 값 2값마스크 후 시프트 · 실제 값 5값마스크 후 시프트마스크 비교 · 의도한 값 1값마스크 비교 · 실제 값 0값마스크 비교비교 연쇄 · 의도한 값 0값비교 연쇄 · 실제 값 1값비교 연쇄

1 << 2 + 3 · lo + (hi - lo) >> 1 · 0x2345 & 0xF000 >> 12 · x & 4 == 4 · 5 < 4 < 3 순서예요

단위: 값 · 자료: Apple clang 17 로 직접 컴파일해 출력 (lo=10, hi=20, x=6)
표로 보기
구분의도한 값실제 값
시프트와 덧셈732
중간값 시프트1510
마스크 후 시프트25
마스크 비교10
비교 연쇄01

몇 가지는 더 볼 점이 있어요.

진법 변환기에 0x2345 와 0xF000 을 넣어 2진수로 보면 세 번째 식에서 왜 5 가 나왔는지 비트 단위로 보여요. 원리는 2진수 16진수 변환 원리 글에 정리해 두었어요.

파이썬·자바스크립트에서만 생기는 우선순위 함정

C 계열과 반대 방향의 함정도 있어요.

파이썬 비교 연쇄. 파이썬 레퍼런스는 x < y <= z 를 x < y and y <= z 와 같게 읽는다고 적어요. 그래서 1 < 3 == 3 은 1 < 3 and 3 == 3 이라 True 이고, (1 < 3) == 3 은 False 예요. == 나 in, is 도 비교 연산자라 연쇄에 끼어든다는 점을 놓치기 쉬워요.

pandas 조건 필터. 파이썬에서는 & 가 비교보다 먼저 묶이니까 df[df.a > 1 & df.b < 2] 는 df.a > (1 & df.b) < 2 라는 연쇄 비교가 돼요. pandas 3.1 에서 돌려 보면 ValueError: The truth value of a Series is ambiguous 가 나요. pandas 인덱싱 문서도 조건마다 괄호로 묶으라고 안내해요. df[(df.a > 1) & (df.b < 2)] 로 써야 해요.

거듭제곱과 단항 마이너스. 파이썬은 ** 가 왼쪽 단항 연산자보다 강하게 묶여서 -2**2 가 -4 예요. 자바스크립트는 아예 문법 오류로 막아요. Node.js 에서 -2 ** 2 를 실행하면 "Unary operator used immediately before exponentiation expression" 오류가 나요. 2**3**2 는 두 언어 모두 오른쪽부터 계산해서 512 예요.

자바스크립트 ?? 섞기. a ?? b || c 는 SyntaxError 예요. MDN 은 ?? 의 피연산자로 ||·&& 를 괄호 없이 쓸 수 없다고 적어요. 언어가 처음부터 "관계를 정하지 않은 쌍"을 둔 사례예요.

자바스크립트 ! 와 instanceof. !obj instanceof Array 는 (!obj) instanceof Array 라 항상 false 예요. ESLint 의 no-unsafe-negation 규칙이 이 패턴을 잡아 주고, 추천 설정에 들어 있어요.

광고
광고

전순서 대신 부분 순서, 괄호를 강제하는 언어들

왼쪽은 C·자바스크립트처럼 모든 연산자 쌍에 순서가 있는 10단계 사다리, 오른쪽은 Zig 처럼 곱셈류·덧셈류·비교·논리 네 그룹만 사슬로 잇고 orelse·catch 같은 사슬 밖 연산자와 섞으면 괄호를 요구하는 부분 순서 구조
전순서는 헷갈려도 컴파일되고, 부분 순서는 헷갈리는 쌍에서 멈춰요

대부분의 언어는 우선순위를 전순서로 정해요. 어떤 두 연산자를 골라도 누가 먼저인지 답이 있어서, 파서는 표만 보면 트리를 만들 수 있어요. 문제는 그 답을 사람이 다 기억하지 못한다는 점이에요.

부분 순서는 일부 쌍에만 관계를 두고, 관계가 없는 쌍이 괄호 없이 만나면 컴파일 오류를 내요. Zig devlog 2026의 10월 24일 글 "Partial Operator Precedence" 에 따르면, 2016년에 열린 Zig 이슈 #114가 master 브랜치에 들어갔어요. 남은 관계는 곱셈류 → 덧셈류 → 비교 → 논리 사슬이고, x orelse y == z 같은 식은 이제 오류예요. 컴파일러는 0.17.x 에서 어떻게 해석됐는지도 노트로 알려 줘요. 100만 줄이 넘는 Zig 코드베이스에서 고쳐야 했던 줄은 353줄이었다고 해요.

사실 이 방식은 Zig 가 처음은 아니에요.

부분 순서의 장점은 애매한 식이 리뷰까지 오지 않는다는 거예요. 대신 기존 코드를 옮길 때 고칠 곳이 생겨요. Zig 는 옛 해석대로 괄호를 넣어 주는 zig fmt --upgrade 를 한 릴리스 동안만 제공하는데, devlog 는 자동 변환보다 오류를 하나씩 손으로 고치길 권해요. 자동 변환은 "원래 해석"을 굳힐 뿐이라, 그 해석이 애초에 버그였다면 버그도 괄호와 함께 남거든요.

컴파일러 경고·린터로 우선순위 버그 잡는 법

부분 순서가 없는 언어라도 도구로 같은 효과를 낼 수 있어요. 언어별로 켤 것을 정리했어요.

언어도구·옵션잡는 조합기본 상태
C·C++ (clang)-Wparentheses, -Wshift-op-parentheses& 와 ==, 시프트와 +, x<=y<=zApple clang 17 은 옵션 없이도 경고
C·C++ (GCC)-Wparentheses헷갈리기 쉬운 중첩, x<=y<=z-Wall 에 포함
러스트clippy precedence산술과 시프트·비트 섞기complexity 그룹, 기본 경고
러스트clippy precedence_bits시프트와 &·`` 섞기restriction 그룹, 직접 켜야 함 (1.86.0~)
자바스크립트·TSno-mixed-operators서로 다른 그룹의 연산자 섞기직접 켜야 함
자바스크립트·TSno-unsafe-negation!a instanceof B, !a in b추천 설정에 포함

GCC 경고 옵션 문서는 -Wparentheses 가 "사람들이 자주 헷갈리는 우선순위의 연산자가 중첩될 때" 경고한다고 적고, x<=y<=z 는 (x<=y ? 1 : 0) <= z 와 같다고 짚어요. 위에서 돌린 C 예제를 clang 으로 컴파일하면 & has lower precedence than ==; == will be evaluated first 처럼 무엇이 먼저 계산되는지까지 알려 줘요.

clippy 린트 목록의 precedence 는 예시로 1 << 2 + 3 이 32, (1 << 2) + 3 이 7 이라고 보여 줘요. precedence_bits 는 0x2345 & 0xF000 >> 12 가 5 인데 (0x2345 & 0xF000) >> 12 는 2 라는 예시를 들어요. 위 차트의 세 번째 식과 같은 값이에요.

ESLint 의 no-mixed-operators 는 ESLint v8.53.0 에서 더 이상 권장되지 않는 규칙이 되어 v11 에서 빠질 예정이에요. 새로 설정한다면 @stylistic 플러그인의 no-mixed-operators를 써요. 기본 그룹은 산술, 비트, 비교, 논리, in/instanceof 다섯 묶음이고, allowSamePrecedence 가 기본 true 라서 같은 단계끼리는 섞어도 통과해요.

괄호를 언제 넣나, 팀 규칙 체크리스트

사칙연산끼리인지, 비트·시프트가 산술이나 비교와 섞였는지, 비교가 두 번 이상 이어졌는지, 단항 연산자가 거듭제곱 앞에 있는지, ??·orelse·삼항이 섞였는지 다섯 질문으로 괄호 여부를 정하는 체크리스트
1번만 그대로 두고 2~5번은 괄호를 넣어요

"괄호가 많으면 시끄럽다"는 반론은 1번으로 풀려요. a + b * c 까지 괄호를 넣으면 진짜 중요한 괄호가 묻히니까 사칙연산끼리는 그대로 둬요. 나머지는 그림의 2~5번에 걸리면 넣어요.

광고
광고

자주 묻는 질문

연산자 우선순위와 결합 방향은 어떻게 다른가요?

우선순위는 서로 다른 연산자가 만났을 때 누가 먼저인지, 결합 방향은 같은 단계 연산자가 이어질 때 왼쪽부터인지 오른쪽부터인지예요. 10 - 5 - 2 는 왼쪽 결합이라 3 이고, 2 ** 3 ** 2 는 오른쪽 결합이라 512 예요. 러스트·스위프트의 비교 연산자처럼 결합 방향 자체가 없어서 이어 쓰면 오류인 경우도 있어요.

괄호를 많이 넣으면 성능이 떨어지나요?

아니에요. 괄호는 파싱할 때 트리 모양을 정하는 데만 쓰이고, 컴파일된 코드나 바이트코드에는 남지 않아요. 같은 의미의 식이면 괄호 유무와 상관없이 같은 결과물이 나와요. 의미를 바꾸는 괄호만 결과물을 바꿔요.

파이썬에서 x & 1 == 0 은 괜찮은가요?

네, 파이썬은 & 가 비교보다 먼저 묶여서 (x & 1) == 0 으로 읽어요. 다만 같은 코드를 C·자바스크립트로 옮기면 x & (1 == 0) 이 돼서 항상 0 이에요. 여러 언어를 오가는 코드라면 파이썬에서도 괄호를 넣어 두는 게 안전해요.

왜 자바에서는 x & 4 == 4 가 컴파일되지 않나요?

자바도 C 처럼 == 를 먼저 계산하지만, 그 결과가 boolean 이라 int 와 & 할 수 없어요. 그래서 타입 오류가 나요. C·자바스크립트는 비교 결과가 숫자로 바뀌어 계산이 이어지기 때문에 오류 없이 틀린 값이 나와요.

우선순위 경고가 너무 많이 나오면 어떻게 하나요?

처음엔 새로 바뀐 줄에만 적용하고, 기존 경고는 목록으로 뽑아 몇 개씩 고쳐요. 괄호를 기계적으로 넣지 말고 원래 의도가 맞는지 테스트로 확인해요. 경고 하나하나가 숨은 버그 후보예요.

정리

연산자 우선순위 버그는 표를 외워서가 아니라 위험한 조합을 알아보고 괄호를 넣는 습관으로 막아요. 사칙연산끼리는 그대로 두고, 비트·시프트가 산술이나 비교와 섞일 때, 비교가 이어질 때, 단항 연산자가 거듭제곱 앞에 올 때, ??·orelse 가 끼어 있을 때만 괄호를 넣으면 돼요. C 의 & 와 == 순서는 역사적 사정으로 남은 것이고, 러스트·Go·스위프트·Zig 는 각자 다른 방법으로 그 함정을 없앴어요.

사람이 매번 기억하기보다 clang·GCC 의 -Wparentheses, clippy precedence, ESLint no-mixed-operators 를 켜고 CI 에서 오류로 바꿔 두는 게 확실해요. 비트 연산 값은 비트 연산 계산기로, 마스크의 비트 위치는 진법 변환기로 바로 확인할 수 있어요.

더 깊이 읽기

이 글은 실무에서 판단할 때 필요한 것 위주로 정리했어요. 같은 주제의 내부 동작 원리는 devboi 기술 블로그의 Zig 우선순위·bbtree·Pinterest LLM — IT 뉴스에서 도식과 코드로 단계별로 풀었어요.

참고한 자료

#연산자 우선순위#비트 연산 우선순위#괄호 생략 버그#시프트 연산자 우선순위#비교 연산자 연쇄#Wparentheses#clippy precedence#no-mixed-operators
← 이전 글Mamba·하이브리드 모델 언제 쓰나, 트랜스포머와 메모리·GPU 비용 비교다음 글 →프로메테우스 용량 계산, scrape_interval·보존 기간·시계열 수 정하는 법
광고

더 둘러보기

💻 개발자 다른 도구

🔥 많이 찾는 도구

🔮 재미로 한 번 더

📝 읽어 볼 만한 글

글 전체 보기 →