방구석에 놔둔 개발 노트

1년차 웹 / 앱 프론트엔드 엔지니어의 좌충우돌 얼렁뚱땅 앞뒤짱구 생존기

2022-10-23: 디버거 사용법, 그리고 스코프와 호출 스택

검사기를 제대로 사용하려면 디버거도 사용할 줄 알아야한다.

디버깅은 코딩을 하는 사람들이라면 가장 중요하게 여겨야하는 것 중 하나다. 부끄럽지만, 나는 버그 픽스를 굉장히 중요시하면서도 크롬 개발자 도구에서 디버거를 제대로 사용해본 경험이 없고 그에 관한 지식이 없었다.

이번 시간에는 그 디버거를 어떻게 활용하는지에 관해 학습을 좀 해보고자 한다.

목차

 ❓디버거를 왜 사용하는가?

 ❗ 그럼, 디버거를 사용해보자.

 📝 범위(Scope)와 콜 스택(Call Stack)?

 📚 이후의 학습 사항

❓ 디버거를 왜 사용하는가?

  • 프로그램, 앱을 만들다보면 우리는 어떤 예상치 못 한 문제가 발생했을 때, 그 문제가 어떻게 발생했는가를 파악하고 싶을 때가 있다.
  • 디버거는 그런 상황에서 우리가 사용할 수 있는 매우 좋은 도구이다.

❗ 그럼, 디버거를 사용해보자.

  • 특정 코드의 테스트를 위한 거라면 굳이 모든 코드를 다 읽게 할 필요는 없지 않은가. 코드 읽는 걸 끊는 부분을 지정해서 한줄씩 읽어가게 할 수 있다. 이 방식을 잘 활용해보자.
  • 일시 정지를 하게 하는 방법은 두 가지가 있다.
  • 중단점(BreakPoint)를 설정하는 방법
    소스 페이지의 특정 구역에 index를 누르면 BreakPoint가 설정된다.

  • 멈추고 싶은 행에서 ‘debugger’를 추가하는 방법

스크립트에 debugger;를 추가하면 디버깅 모드가 실행되면서 코드가 중단된다.

⚠️ 만약, 본인의 브라우저에서 소스창에 코드 수정이 안 된다면, 아래의 내용을 한 번 보자.

브라우저를 코딩 에디터로 사용하기

⚠️ BreakPoint는 여러 개를 설정할 수 있다. 적재적소로 잘 사용하여, 자신의 코드 문제를 해결하는데 잘 활용해보자.

  • BreakPoint를 설정했으니 디버그 모드를 실행해보자.

위의 코드에서 몇 가지 코드를 더 추가한 뒤, BreakPoint를 조금 더 추가했다. 이제 우측 상단에 있는 ‘스크립트 실행 재개’ 버튼을 눌러 코드가 어떻게 실행되는지 보자.

그러면 이미지와 같이 BreakPoint로 설정한 지점에서 스크립트 읽기가 일시중지될 것이다. 이제, ‘다음 함수 호출(Step Over)’ 버튼을 눌러보자.

아래와 같이 범위(Scope) 항목에서 스크립트 내 우리가 선언한 변수값의 변화가 적용되어 있는 것을 확인할 수 있다.

이제, 다시 처음에 눌렀던 ‘스크립트 실행 재개’ 버튼을 눌러보면서 스크립트 내 변수 값이 어떻게 변화하는지를 확인해보자.

‘스크립트 실행 재개’ 버튼을 누르면 다음 BreakPoint 지점까지 이어진 코드들을 전부 읽어준다. 마지막 BreakPoint에서 누를 경우, 읽지 않은 남은 코드들을 전부 읽어 디버깅 모드를 종료한다.

이처럼 디버깅 모드에서 테스트를 할 때, 위의 기능들을 잘 활용한다면, 어느 코드에서 문제점이 있는지를 파악하기가 쉬워질 것이다.

  • Debugger의 진면목은 함수를 디버깅할 때에 나온다.

우선, 아래와 같이 코드를 작성해본 뒤 BreakPoint를 설정해봤다.

위처럼 코드를 작성한 다음, ‘다음 함수 호출(Step Over)’ 버튼을 눌러보자. 18번째 줄인 fn();으로 이동하게 된다. 그 다음, 또 ‘다음 함수 호출(Step Over)’ 버튼을 누르면 디버깅 모드가 끝난다. 이런.

그럼, 이 경우 함수 안에 있는 코드를 한 줄 씩 읽게 하고 싶은데 어떻게하면 될까? 이 때, Step Over 옆에 있는 ‘다음 함수 호출(Step Into)’ 버튼을 누르면 된다.

⚠️ Step Over 버튼과 Step Info 버튼의 한국어 번역 문구가 동일하므로, 아이콘 이미지와 영단어로 헷갈리지 않도록 하자!

Step Into 버튼을 누르면 ‘로컬’이라는 단어와 함께, 범위에 b가 undefined로 출력되는 것을 볼 수 있다. 여기서 다시 한 번 Step Into 버튼을 누르면…

범위의 로컬 변수 b의 값이 변화된 것을 볼 수 있다. 이런 식으로 함수 안이냐, 바깥이냐에 따라 범위(Scope) 안에서의 변수의 변화 과정을 디버깅을 통해서 확인할 수 있다.

이것 이외에도 우리가 또 짚고 갈 사항이 하나 더 있는데, 바로 ‘호출 스택(Call Stack)’이라는 친구다.

위에 있던 두 이미지를 다시 가져와봤는데, 여기서 ‘호출 스택’을 한번 보자. ▶(익명)이라고 나와있던 호출 스택이 어느 새인가 ▶fn이 생겨나있다.

호출 스택에 있는 ▶는 바로 현재 디버깅으로 읽고 있는 순서가 어느 위치에 해당하는지를 알려주고 있다.

즉, ▶fn이라고 표시되는 것은 현재 디버깅 과정 중에 fn 함수 속으로 들어왔다는 뜻인데 이것은 현재 fn이라는 함수를 실행하고 있는 중임을 알려준다.

그 와는 다르게 (익명)이라고 나와있는 호출 스택은 별도의 함수가 읽혀지지 않은 상황이기 때문에, 이것은 ‘전역에서 실행 중’이라는 걸 말한다.

📝 범위(Scope)와 콜 스택(Call Stack)?

위에서 디버깅을 하는 방법을 배우던 중, 우리는 JavaScript와 관련하여 몇 가지 개념들을 알게 됐다. 여기서는 그 개념들을 복습해보고자 한다.

범위(Scope)

  • 한 마디로 변수에 접근할 수 있는 범위를 가리킨다.
  • Scope에는 두 가지가 있다. 전역(Global)과 지역(Local)이다.
  • 전역 스코프 (Global Scope)
    • 전역에 선언되어 있기 때문에 어느 곳이든 이 변수에 접근할 수 있다.
  • 지역 스코프 (Local Scope)
    • 지정한 범위에서만 접근할 수 있어, 해당 범위를 벗어날 경우 접근할 수 없다.
  • 함수를 선언할 때마다 새로운 Scope를 선언하게 되는데, 이 경우, 함수 안에서 선언한 변수는 함수 안에서만 사용할 수 있다.
    • 바로 이 함수에서 선언하는 것들지역 스코프의 예시가 되겠다.
    • 그리고 이 함수를 이용한 지역 스코프를, 함수 스코프(Function Scope)로 부르기도 한다.

호출 스택(Call Stack)

  • 호출 스택, 콜 스택이라고 부르는 이것은 함수를 호출(Function Stack)했을 경우, 해당 내용을 추적할 때에 사용되는 작동 구조 되시겠다.
  • 위에서 공부했던 이미지 중 하나를 가져와봤다. 보면서 이야기해보자.

위의 코드는 아래의 순서를 가진다.

  1. Script 내용이 실행되면 let a = 1;코드를 먼저 읽어온다.
  2. 이후 fn()에 도달할 때까지, 그 사이에 적힌 코드들은 무시된다.
  3. fn()에 도달했다면, 해당 함수를 호출한다.
  4. 이후, fn()호출 스택 리스트에 추가된다.

여기까지만 봤을 때의 내용을 정리하자면 아래와 같다.

스크립트가 함수를 호출하면 인터프리터는 이를 호출 스택에 추가한 다음 함수(fn())를 수행하기 시작합니다.

계속해서 과정을 이어가보자.

  1. fn() 내부의 모든 코드를 실행한다.
  2. fn() 내부의 모든 코드를 다 실행했다면, fn()이 호출된 라인으로 돌아온다.
  3. 호출 스택 리스트에 fn()을 제거하고, fn()이 호출된 라인 이후 남은 코드들을 실행한다.
  4. 모든 코드들이 실행됐다면 Script 내용을 마친다.

여기까지 본 내용을 추가로 정리하자면 아래와 같다.

메인 함수가 끝나면 인터프리터는 스택을 제거하고 메인 코드 목록에서 중단된 실행을 다시 시작합니다.

만약, fn()이라는 함수 안에 fn2()라는 함수와 코드가 있다고 하자. 그러면 어떻게 될까? 5번과 6번 사이에 다음과 같은 과정이 추가될 것이다.

5-1. fn2()에 도달할 때까지, fn() 내부의 코드들을 실행한다. 사이에 있는 fn2() 코드들은 무시한다.

5-2. fn2()에 도달했다면 해당 함수를 호출한다.

5-3. 이후, fn2()가 호출 스택 리스트에 추가된다.

5-4. fn2() 내부의 모든 코드를 실행한다.

5-5. fn2() 내부의 모든 코드를 다 실행했다면, fn2()가 호출된 라인으로 돌아온다.

5-6. 호출 스택 리스트에 fn2()를 제거하고,fn() 속에서 fn2()가 호출된 라인 이후 남은 코드들을 실행한다.

  1. fn() 내부의 모든 코드를 다 실행했다면, fn()이 호출된 라인으로 돌아온다.

5-6번의 사이에 위의 내용들이 추가된다고 보면 된다. 위의 과정이 연상이 되지 않는다면 아래의 이미지를 보자.

5-1~5-7의 과정을 한 문장으로 정리하자면 아래와 같다.

해당 함수(fn())에 의해 호출되는 모든 함수(fn2())는 호출 스택에 추가되고 호출이 도달하는 위치에서 실행합니다.

그래서, 호출 스택(Call Stack)의 구조는 다음과 같이 정리할 수 있다.

  1. 스크립트가 함수를 호출하면 인터프리터는 이를 호출 스택에 추가한 다음 함수(fn())를 수행하기 시작합니다.
  2. 해당 함수(fn())에 의해 호출되는 모든 함수(fn2())는 호출 스택에 추가되고 호출이 도달하는 위치에서 실행합니다.
  3. 메인 함수가 끝나면 인터프리터는 스택을 제거하고 메인 코드 목록에서 중단된 실행을 다시 시작합니다.

만약, 스택이 자신이 할당받은 범위보다 많은 공간을 차지하게 된다면 어떤 일이 벌어질까?

아래의 코드를 보자.

function stack() {
    stack();
}
stack();

함수 안에 자신을 부르는 이 함수를 흔히 우리는 ‘재귀함수’라고 부른다. 영어에서 들어봤을 법한 ‘재귀대명사’같은 존재처럼 말이다.

위의 함수는 stack()을 계속해서 불러낼 것이다. 끝도 없이 말이다. 만약, 저 함수가 작동하게 된다면 어떻게 될까?

Maximum call stack size exceeded, 콜 스택으로 부를 수 있는 최대치를 넘어섰다는 에러가 뜬다. 이것이 바로 흔히들 들어본 ‘스택 오버플로우(stack overflow)’가 되시겠다.

호출 스택을 이용해서 함수를 관리하는 것은 자바스크립트에서 자주 쓰이는 방식이다. (특히 React의 Component를 생각해보자.)

하지만, 스택이 할당된 공간보다 많이 차지하게 된다면 ‘stack overflow’가 발생할 수 있으므로 이 부분에 유의하여 호출 스택과 스코프를 적재적소를 잘 활용하도록 하자.

📚 이후의 학습 사항

  • 함수 안의 함수? 외부 함수와 내부 함수, 그리고 닫기(Closure)
  • 자바스크립트는 어떤 식으로 실행되는 걸까? 실행 컨텍스트(Execute Context)
  • stack? que? 기본 알고리즘인 두 가지를 짚고 가자.

[학습 및 참고 자료]

크롬 개발자 도구 - 자바스크립트 디버거

(JavaScript) 스코프(Scope)란?

호출 스택 - 용어 사전 | MDN

자바스크립트 호출 스택(Call Stack) 이해하기