최근 수정 시각 : 2023-08-20 18:30:15

#!

[include(틀:링크시 주의, 링크=\ 또는 #)]
1. 개요2. 사용
2.1. 셸 스크립트에서2.2. 기타 스크립팅2.3. 단일 페이지 어플리케이션에서의 URL hack
3. 여담4. 관련 문서5. 외부 링크

1. 개요

shebang

UNIX에서 전통적으로 사용하는 실행 경로 지정자. 쉬뱅이라고 발음하며 흔히 해시뱅, 샤뱅, 샵(sharp)뱅 등으로도 읽는다.

2. 사용

2.1. 셸 스크립트에서

전통적으로 유닉스는 실행 파일에 확장자를 붙히지 않는 것이 관례[1]이다. 또한 일반 파일도 굳이 파일 확장자만으로 종류를 구분하지는 않는다. 따라서 실행 파일은 대부분의 경우 확장자가 없이 파일명만 존재했으며, 별도의 해석기(인터프리터)가 필요한 경우, 파일 맨 위쪽에 주석으로 #!<인터프리터 경로명>을 써두면 실행시에 이 부분을 읽어들여 적절한 해석기로 넘겨주는 역할을 했다.

예를 들어 배시 스크립트로 짠 파일을 실행하고 싶다면 다음과 같이 짤 수 있다.
#!syntax sh
#!/bin/bash
echo "Hello world!"
위 내용을 hello라는 이름으로 저장하고 실행 권한을 준 다음에 ./hello로 실행하면 표준 출력으로 Hello world!가 출력된다.

2.2. 기타 스크립팅

조금 더 구체적인 예로, Node.js가 설치된 환경에서 주어진 이름으로 폴더를 만드는[2] 간단한 스크팁트 CLI툴을 만드는 예시를 생각해 보자. 아마 코드는 다음과 같을 것이다.
#!syntax javascript
const { mkdir } = require('node:fs/promises')
!async function() {
    await Promise.all(
        process
            .argv
            .slice(2)
            .map(mkdir)
    )
}()
이 내용을 nodedir등 적당한 이름으로 저장하고 node ./nodedir hello world를 입력한다면 hello[3]world라는 폴더가 각각 생길 것이다. 하지만 이를 스크립트로 계속 사용하고 싶다면 문제가 발생할 것이다.

이때도 똑같이 맨 윗줄에 샤뱅 코드를 넣는다면[4] 셸 스크립트처럼 실행이 가능해진다.
#!syntax javascript
#!/usr/bin/node
const { mkdir } = require('node:fs/promises')
...
실행하려면#!syntax sh ./nodedir hello world이제 해당 스크립트를 PATH에 넣어서 어느때나 사용 가능한 스크립트로 쓸 수 있다.

여기서 발생할 수 있는 한 가지 문제는 node가 항상 /usr/bin/node에 있지 않을 수도 있다는 것이다. /usr/local/bin에 존재하거나 별도의 노드 버전 매니저를 사용한다면 수시로 달라질 수 있다. 이때 env명령어를 사용하는데 env는 현재 PATH로부터 주어진 실행 파일을 자동으로 찾는다. 따라서 #!/usr/bin/env node를 사용한다면 실행 파일 위치에 관계없이 항상 인터프리터 경로를 찾을 수 있게 된다.

shebang의 원리를 효과적으로 사용하면 여러 꼼수를 부릴 수 있는데, 예를 들어 Node.js는 하위 호환성을 위해 ESM을 별도의 설정 없이는 지원하지 못한다. 따라서 import 문법 등을 사용하면 다음과 같은 에러가 발생하게 된다.
(node:2343136) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
import { mkdir } from 'node:fs/promises'
^^^^^^

SyntaxError: Cannot use import statement outside a module
실행 파일은 확장자를 사용하지 않을 것이기에 .mjs 확장자를 쓸 수도 없고, 어느 위치에서나 사용할 수 있도록 package.json에 의존해서도 안 된다. 이럴 때 다음처럼 중간에 sh로 해석하는 부분을 끼워넣어 해결할 수 있다.
#!syntax javascript
#!/bin/sh
':' // ; cat "$0" | node --input-type=module - $@ ; exit $?
import { mkdir } from 'node:fs/promises'
await Promise.all(
    process
        .argv
        .slice(2)
        .map(mkdir)
)
//이후의 내용은 자바스크립트 주석으로 인식되어 코드의 해석에 영향을 미치지 않는다. 하지만 반대로 셸로 이 파일을 실행하면 //은 무시되고 cat을 통해 현재 파일을 표준 입력으로 보낸다. 동시에 node에 --input-type=module 인자를 주어 현재 파일을 ES module로 해석하도록 만들 수 있다.

js외에도 흔히 쓰이는 Ruby, Lua등의 언어를 사용할 수 있으며, 그중 가장 많이 쓰이는 것은 단연코 Perl이다. 유닉스의 수많은 스크립트 파일이나 훅 등을 보면 상당 부분이 펄로 짜여져 있고, 최상단에 shebang코드가 있는 것을 볼 수 있다.

2.3. 단일 페이지 어플리케이션에서의 URL hack

트위터의 사례

과거 브라우저에서 URL을 변경하면 요청이 초기화되고 페이지 전체가 새로고침 되어야 했던 시절, SPA를 구현하기 위해 링크를 눌러도 페이지의 새로고침을 막아 자연스러운 UI 트랜지션을 보여주는 것이 중요했다.

다만 이렇게 하면 웹서핑을 하는 도중에도 항상 URL이 그대로 고정되고, 나중에 특정 글 등의 링크를 복사해서 보내거나 하는 것에 어려움이 생긴다. 이를 위해 HTML5 표준으로 history push 기능이 생겨 현재는 pjax 등의 패러다임이 대세가 되고 있지만, 그 이전에는 현실적으로 모든 브라우저가 같은 기능을 지원함을 보장할 수 없었다. 이를 위해서 나온 거 바로 URL fragments를 사용하는 hashbang hack이다.

위의 예시에서 그대로 이어서, 원하는 글에 fragment anchor를 걸어 URL이 리로딩 없이 업데이트 되도록 한 후, 나중에 완전히 fresh한 요청에서 해당 fragment로 가는 URL이 요청될 경우 클라이언트 측의 자바스크립트가 해당 데이터만 AJAX로 패치하는 방식으로 부분적인 SPA를 구현할 수 있다.

과거 react-router등에 사용된 #가 들어간 URL등도 전부 같은 방식이다.

위의 유닉스식 해시뱅과 직접적인 연관은 없다.

3. 여담

나무마크에도 shebang에 영향을 받은 듯한 문법이 존재한다.
{{{#!wiki style="속성 목록"
속성이 적용된 텍스트}}}

4. 관련 문서

5. 외부 링크



[1] 어디까지나 관례이기 때문에 의무는 아니다.[2] 내장 명령어인 mkdir과 비슷한 동작이다.[3] 이전 예제에서 hello 파일을 지우지 않았다면 EEXIST예외가 발생할 것이다. 다만 Promise.all을 사용하기에 world생성은 성공하니 상관은 없다.[4] js의 주석은 #이 아니지만 언어에 관계없이 보통 맨 윗줄의 샤뱅을 인식하고 무시한다.

분류