자바스크립트의 억까
아직 휴가라 일하는 날은 아닌데 딱히 땡기는 작업이 없어서 그냥 swc 작업했다.
swc의 미니파이어가 react-joyride
를 깨는 문제를 보고 있었는데, 진짜 어이없는 케이스라는 걸 깨달았다.
자세한 설명은 github.com/swc-project/swc/pull/6509 에 적어놓았으니 여기선 간략하게 설명하겠다.
입력 코드는 다음과 같다.
var fragments = offset.split(/(\+|\-)/).map(function (frag) {
return frag.trim();
});
var divider = fragments.indexOf(
find(fragments, function (frag) {
return frag.search(/,|\s/) !== -1;
})
);
if (fragments[divider] && fragments[divider].indexOf(",") === -1) {
console.warn(
"Offsets separated by white space(s) are deprecated, use a comma (,) instead."
);
}
그리고 문제되는 룰은 a = foo, use(b(a))
를 use(b(a = foo))
처럼 바꾸는 룰이었다.
swc minifier는 저거 압축할 때 사이드 이펙트나 변수 간의 관계를 다 따진다.
근데 resolution 순서를 따지진 않았다.
여기서 골 때리는 버그가 생겨났다.
위의 코드에 저 룰을 한번 적용하면
var fragments;
var divider = (fragments = offset.split(/(\+|\-)/).map(function (frag) {
return frag.trim();
})).indexOf(
find(fragments, function (frag) {
return frag.search(/,|\s/) !== -1;
})
);
if (fragments[divider] && fragments[divider].indexOf(",") === -1) {
console.warn(
"Offsets separated by white space(s) are deprecated, use a comma (,) instead."
);
}
처럼 최적화된다.
그런데 저 룰을 한번 더 적용할 수 있는 것처럼 보인다.
divider
에 할당하는 식이 존재하고 fragments[divider]
가 존재하니까.
그리고 대부분의 경우에 이건 올바른 최적화가 맞다.
문제는 저게 올바르지 않은 예시 케이스가 존재한다는 것이다.
코드 실행 순서와 resolution 순서가 달라서 그렇다.
fragments[divider = (fragments = foo)]
의 실행 순서는 foo
=> fragments = foo
=> divider = (fragments = foo)
=> fragments
=> fragments[divider = (fragments = foo)]
이다.
이거엔 문제가 없다.
문제는 resolution 순서이다.
자바스크립트에서는 [
왼쪽의 fragments
가 먼저 resolve 된다.
따라서 undefined[divider = (fragments = foo)]
가 되어버리고, 런타임에 익셉션이 발생한다.
참고로 이 문제는 디버거를 붙여도 뭐가 undefined
인지 안 보여준다.
익셉션이 발생한 시점은 fragments[divider = (fragments = foo)]
를 실행하는 시점이고 이 시점에 fragments
는 올바른 값을 갖고 있다.
그래서 억까라고 하는 것이다.
디버거에 낚여서 삽질한 거라 짧게 기록해놓기로 했다.