1. 정의 / 결론

sh최소 공통 쉘 인터페이스에 가깝고, bash는 그 위에 확장 기능을 추가한 쉘이다.
이 차이는 단순한 이름 차이가 아니라, 스크립트가 어디서 깨지고 어디서 안전하게 동작하는지를 결정하는 실행 기준의 차이다.

2. 핵심 요약

sh는 호환성과 이식성을 우선한다.
bash는 배열, [[ ]], brace expansion 같은 확장 문법을 제공한다.
스크립트가 여러 환경에서 돌아야 하면 sh, 기능이 더 필요하고 실행 환경을 통제할 수 있으면 bash가 맞다.

3. 왜 필요한가

문제는 shbash가 겉으로는 비슷하게 보인다는 데서 시작한다. 터미널에서 둘 다 명령어를 실행한다. if, for, 변수 선언도 얼핏 비슷하다. 그래서 처음에는 둘을 같은 것으로 취급하기 쉽다. 하지만 스크립트가 길어지고 조건 분기, 배열, 문자열 처리, 배포 자동화가 들어가기 시작하면 차이가 바로 드러난다. 로컬에서는 잘 실행되던 코드가 운영 서버에서 갑자기 실패하는 경우가 대표적이다.

기존 한계는 개발 환경과 운영 환경이 다를 수 있다는 점이다. 작성자는 macOS나 개인 리눅스 환경에서 bash를 기본 쉘로 쓰고 있을 수 있다. 그런데 실제 실행 환경은 /bin/shdash를 가리키는 경량 쉘일 수 있다. 이 경우 작성자는 자신이 bash 문법을 사용하고 있다는 사실조차 의식하지 못한 채 스크립트를 작성하고, 운영 환경은 그 문법을 해석하지 못해 오류를 낸다. 문제의 핵심은 문법 자체보다 실행 환경에 대한 가정이 코드에 숨어 있다는 점이다.

해결 구조는 단순하다. 먼저 스크립트가 어느 수준의 호환성을 목표로 하는지 정해야 한다. 모든 POSIX 계열 환경에서 최대한 안전하게 돌려야 한다면 sh 기준으로 작성해야 한다. 반대로 실행 환경이 명확하게 bash이고, 배열이나 확장 조건식이 필요하다면 bash를 명시적으로 선언해야 한다. 이렇게 기준을 먼저 고정하면 문법 선택, shebang 작성, 테스트 방식까지 한 줄로 이어진다.

실제 영향은 배포 스크립트, Docker entrypoint, CI/CD job, init 스크립트 같은 곳에서 나타난다. 이런 코드는 애플리케이션 본문보다 짧아 보이지만, 실패할 경우 시스템 전체 기동이 막힌다. 결국 shbash의 차이는 쉘 취향 문제가 아니라 운영 안정성과 재현 가능성의 문제다.

4. 예제

예제 1. [[ ]][ ] 차이

#!/bin/bash
name="admin"

if [[ "$name" == "admin" ]]; then
  echo "ok"
fi

결과는 bash에서 ok가 출력된다.

이 코드가 동작하는 이유는 [[ ... ]]bash가 제공하는 확장 조건식이기 때문이다. 문자열 비교, 패턴 매칭, 연산자 사용이 좀 더 편하다. 하지만 이 문법은 POSIX 표준 sh 문법이 아니다.

같은 코드를 sh에서 안전하게 쓰려면 다음 형태가 맞다.

#!/bin/sh
name="admin"

if [ "$name" = "admin" ]; then
  echo "ok"
fi

실무 의미는 명확하다. 여러 서버에서 돌 배포 스크립트라면 두 번째 방식이 더 안전하다. 첫 번째 방식은 bash를 명시적으로 요구하는 코드다.

예제 2. 배열 사용

#!/bin/bash
arr=("apple" "banana" "cherry")
echo "${arr[0]}"
echo "${arr[1]}"

결과는 apple, banana가 순서대로 출력된다.

이렇게 되는 이유는 bash가 배열 타입을 지원하기 때문이다. 인덱스로 접근할 수 있고, 반복 처리도 비교적 자연스럽다. 반면 sh는 이런 배열 문법을 전제로 설계된 쉘이 아니다. sh에서 같은 방식으로 작성하면 문법 오류가 나거나 기대한 결과가 나오지 않는다.

sh에서는 보통 위치 인자나 문자열 분리를 사용한다.

#!/bin/sh
set -- apple banana cherry
echo "$1"
echo "$2"

실무 의미는 데이터 구조가 조금만 복잡해져도 bash가 훨씬 편해진다는 점이다. 대신 그 편의성은 bash 런타임이 존재한다는 전제를 요구한다.

예제 3. brace expansion

#!/bin/bash
echo file_{1..3}.log

결과는 file_1.log file_2.log file_3.log처럼 확장된다.

이 결과가 나오는 이유는 bash가 brace expansion을 수행하기 때문이다. 코드를 짧게 쓰기에는 편하지만, 이 기능은 sh에서 보장되지 않는다. 따라서 /bin/sh가 다른 경량 쉘로 연결된 환경에서는 그대로 문자열이 출력될 수 있다.

sh에서는 더 보수적으로 반복문을 작성해야 한다.

#!/bin/sh
i=1
while [ "$i" -le 3 ]
do
  echo "file_${i}.log"
  i=$((i + 1))
done

실무 의미는 짧은 문법보다 재현 가능한 동작이 더 중요한 환경에서는 sh 스타일이 유리하다는 점이다.

예제 4. shebang 차이

#!/bin/sh
echo "hello"

이 스크립트는 운영체제에 /bin/sh로 실행하라는 신호를 준다.

#!/bin/bash
echo "hello"

이 스크립트는 /bin/bash로 실행하라는 의미다.

두 코드의 출력은 같을 수 있다. 하지만 해석기는 다르다. 따라서 본문에 들어갈 수 있는 문법 범위도 달라진다. 핵심은 결과가 같아 보이는 간단한 예제일수록 차이를 놓치기 쉽다는 점이다. 실제로는 첫 줄 한 줄이 스크립트의 언어 사양을 결정한다.

실무 의미는 스크립트의 문법과 shebang이 일치해야 한다는 것이다. bash 문법을 쓰면서 #!/bin/sh를 넣는 순간, 코드는 의도와 선언이 분리된다.

5. 실무 적용

1) Docker entrypoint 스크립트

상황은 컨테이너가 기동될 때 짧은 쉘 스크립트로 환경 변수를 확인하고 프로세스를 실행하는 구조다.
문제는 베이스 이미지가 가벼울수록 bash가 아예 없을 수 있다는 점이다. Alpine 계열 이미지에서 특히 자주 나타난다.
적용 방법은 entrypoint를 가능한 한 sh 기준으로 작성하는 것이다. [[ ]], 배열, brace expansion 같은 문법을 피하고, POSIX 호환 문법으로 제한한다.
효과는 이미지 의존성이 줄고, 런타임이 달라도 스크립트가 더 예측 가능하게 동작한다.

2) CI/CD 배포 스크립트

상황은 GitHub Actions, Jenkins, GitLab CI 같은 환경에서 배포용 쉘 스크립트를 실행하는 경우다.
문제는 실행 에이전트가 어떤 쉘을 기본으로 쓰는지 팀원이 정확히 통일해서 알고 있지 않을 수 있다는 점이다.
적용 방법은 bash 기능이 필요하면 shebang에서 #!/bin/bash를 명시하고, 파이프라인에서도 bash로 실행하도록 고정하는 것이다.
효과는 로컬 테스트와 CI 실행 결과의 차이를 줄일 수 있다. 원인 불명 배포 실패가 아니라, 명시된 인터프리터 기반 실패로 바뀐다.

3) 시스템 초기화 스크립트

상황은 서버 부팅 과정이나 서비스 실행 전에 환경 준비용 스크립트를 돌리는 경우다.
문제는 이런 위치의 스크립트는 환경이 가장 제한적일 수 있다는 점이다. 경량 쉘, 제한된 PATH, 최소 패키지 상태가 흔하다.
적용 방법은 가능한 한 sh 기준으로 유지하고, 외부 도구 의존성도 최소화하는 것이다.
효과는 장애 지점이 줄어든다. 애플리케이션이 아니라 초기화 단계에서 실패하는 문제를 줄일 수 있다.

4) 개발 보조 스크립트

상황은 로컬 개발자가 자주 실행하는 빌드, 테스트, 로그 수집 스크립트다.
문제는 이런 스크립트에 문자열 처리, 배열, 함수, 반복 조합이 많아지면 sh만으로는 표현이 거칠어질 수 있다는 점이다.
적용 방법은 아예 bash를 선언하고, 필요한 확장 문법을 적극적으로 사용하는 것이다.
효과는 코드가 짧아지고 읽기 쉬워진다. 단, 이 경우 실행 환경을 팀 차원에서 명확히 맞춰야 한다.

6. 흔한 실수

실수 1. #!/bin/sh로 시작했는데 [[ ]]를 사용함

잘못된 사용은 선언은 sh인데 본문은 bash 문법으로 작성하는 것이다.

#!/bin/sh
if [[ "$x" = "1" ]]; then
  echo ok
fi

실제 결과는 sh 환경에서 [[ not found나 조건식 오류가 발생할 수 있다.

왜 틀렸는지는 shebang이 해석기를 sh로 고정했기 때문이다. 본문 문법은 bash를 요구하지만 선언은 sh다. 코드와 실행기가 서로 다른 언어를 말하고 있는 셈이다.

올바른 방법은 둘 중 하나다. 문법을 POSIX 형태로 바꾸거나, 아예 #!/bin/bash로 선언을 바꿔야 한다.

실수 2. 로컬에서만 테스트하고 배포함

잘못된 사용은 개발자 로컬 터미널에서 실행이 되면 서버에서도 된다고 가정하는 것이다.

실제 결과는 macOS나 사용자 쉘이 bash일 때는 정상이고, 운영 서버의 /bin/shdash일 때는 실패하는 방식으로 나타난다.

왜 틀렸는지는 테스트 환경과 실행 환경이 다르기 때문이다. 쉘 스크립트도 결국 런타임 의존 코드다. 인터프리터가 달라지면 언어 사양도 달라진다.

올바른 방법은 shebang에 맞는 해석기로 직접 테스트하는 것이다. sh script.sh, bash script.sh를 각각 돌려보면 숨겨진 의존성이 빠르게 드러난다.

실수 3. 배열이 되는 줄 알고 sh 스크립트에 사용함

잘못된 사용은 아래와 같다.

#!/bin/sh
arr=("a" "b")
echo "${arr[0]}"

실제 결과는 문법 오류다.

왜 틀렸는지는 sh가 배열을 기본 기능으로 제공하지 않기 때문이다. bash에서는 자연스러운 코드가 sh에서는 언어 차원에서 성립하지 않는다.

올바른 방법은 위치 인자, 문자열 분리, 반복문 구조로 다시 설계하는 것이다. 데이터 구조가 복잡하면 그 스크립트는 bash가 더 적합한 신호일 수 있다.

실수 4. /bin/sh가 항상 bash라고 가정함

잘못된 사용은 모든 리눅스에서 /bin/sh가 사실상 bash일 것이라고 보는 것이다.

실제 결과는 배포판마다 동작이 달라진다. 어떤 환경은 우연히 통과하고, 어떤 환경은 즉시 실패한다.

왜 틀렸는지는 /bin/sh는 경로 이름일 뿐이며, 실제 구현체는 시스템마다 다를 수 있기 때문이다. Debian 계열은 dash를 사용하는 경우가 대표적이다.

올바른 방법은 /bin/sh를 최소 공통 인터페이스로 보고 작성하는 것이다. bash가 필요하면 경로도 명확히 bash로 지정해야 한다.

실수 5. 짧은 문법을 우선하고 호환성을 놓침

잘못된 사용은 {1..10}, source, 고급 문자열 치환 같은 기능을 습관적으로 넣는 것이다.

실제 결과는 스크립트가 짧아지지만 특정 환경에서만 동작한다.

왜 틀렸는지는 짧은 문법이 대부분 확장 기능이기 때문이다. 편의 기능은 실행 환경이 통제될 때만 안전하다.

올바른 방법은 스크립트의 목적부터 분리하는 것이다. 시스템 공용 스크립트는 sh, 개발 편의 스크립트는 bash로 역할을 나누면 기준이 명확해진다.

실수 6. sourcesh에서도 되는 줄 앎

잘못된 사용은 아래와 같다.

#!/bin/sh
source ./env.sh

실제 결과는 source: not found가 발생할 수 있다.

왜 틀렸는지는 sourcebash 계열에서 자주 쓰이는 문법이기 때문이다. POSIX 쪽에서는 보통 . 명령을 사용한다.

올바른 방법은 sh 기준이면 다음처럼 작성하는 것이다.

. ./env.sh

이 실수는 작아 보이지만 환경 변수 로딩 단계에서 바로 실패하므로 영향이 크다.

7. 관련 개념

POSIX
유닉스 계열 시스템에서 공통 동작을 맞추기 위한 표준 규격이다. sh 호환성 이야기는 대부분 POSIX와 연결된다.

shebang
스크립트 첫 줄의 #! 선언이다. 어떤 인터프리터로 파일을 실행할지 결정한다.

dash
Debian 계열에서 /bin/sh로 자주 연결되는 경량 쉘이다. 빠르고 단순하지만 bash 확장 기능은 기대할 수 없다.

interactive shell / non-interactive shell
사람이 직접 터미널에서 쓰는 쉘과, 스크립트를 실행하는 비대화형 쉘은 환경 로딩 방식이 다를 수 있다. 동일 명령도 다르게 보일 수 있는 이유다.

8. 더 깊이 보기

구조적으로 보면 shbash의 차이는 단지 기능 수 차이가 아니다. sh는 공통 분모를 확보하는 방향으로 설계가 읽힌다. 즉, 환경이 달라도 최대한 비슷하게 동작하는 것을 우선한다. 반대로 bash는 사용자 편의와 표현력 확장을 선택했다. 이 선택 덕분에 조건식, 반복, 문자열 처리, 배열에서 훨씬 풍부한 표현이 가능해졌다. 결국 두 쉘은 목표가 다르다.

시스템 관점에서는 이 차이가 배포 가능성과 직결된다. 운영 환경은 보통 개발 환경보다 더 보수적이다. 더 적은 패키지, 더 단순한 런타임, 더 제한된 PATH가 일반적이다. 따라서 운영 스크립트는 애플리케이션 코드보다 오히려 더 보수적인 문법을 요구받는다. 여기서 sh의 역할이 생긴다. 기능은 적지만, 그 제한이 오히려 환경 차이를 줄이는 장치가 된다.

반대로 팀 내부 도구나 개발 생산성 스크립트는 상황이 다르다. 이 경우 실행 환경을 팀 차원에서 통제할 수 있다면 bash의 확장 기능이 비용보다 이득이 크다. 배열과 고급 조건문 덕분에 코드 양을 줄이고 가독성을 높일 수 있다. 즉, 어느 쪽이 우월한가가 아니라 어느 층위의 문제를 해결하는가가 다르다. sh는 이식성 문제를 줄이고, bash는 표현력 문제를 줄인다.

따라서 실전 기준은 문법 취향이 아니라 시스템 경계다. 시스템 바깥으로 나갈수록 sh가 유리하고, 통제 가능한 안쪽으로 들어올수록 bash가 유리하다. 이 기준을 먼저 세우면 스크립트 설계가 훨씬 일관된다.

9. 정리

sh는 최소 공통 기준이고, bash는 확장 기능이 있는 쉘이다.
차이는 문법 취향이 아니라 호환성 범위와 실행 환경의 차이다.
스크립트가 넓은 환경에서 살아남아야 하면 sh, 더 풍부한 기능이 필요하고 환경을 통제할 수 있으면 bash가 맞다.
가장 흔한 장애 원인은 문법보다 선언과 실행 환경의 불일치다.
결론은 하나다. 스크립트를 쓰기 전에 먼저 그 파일이 sh 스크립트인지 bash 스크립트인지부터 확정해야 한다.