우선 재귀하면 가장 먼저 떠오르는 팩토리얼(!) 예제를 일반적인 재귀, 꼬리 재귀로 구현했을 때 어떤 차이가 있는 지 한번 비교해보자. (개발자 도구를 열어 직접 디버깅을 하며 스택이 어떤 식으로 변하는지 직접 눈으로 확인해보면 더 이해하기 쉬울 것이다)

일반 재귀 방식

functionfactorial(n) {
if (n === 1) {
return 1;
    }
return n * factorial(n-1);
}

n값으로 3을 넣었을 때, 스택이 어떻게 변하는 지를 그림으로 그려보았다. 빨간색으로 표시된 부분까지는 호출당한 함수들이 쭉 스택에 쌓인다. 그리고 노란색으로 표시된 부분부터는 각자가 자신이 계산한 값들을 반환한다.

그 밑에 있는 그림은 스택에 쌓여있는 함수들을 사람으로 표현해본 그림이다. 호출 당한 함수가 호출한 함수에게 자신의 결과값을 알려주고, 그 값을 전달 받은 함수는 자기가 원래 갖고있던 값과 곱셈 연산을 해서 다시 다른함수에게 전달하는 모습을 볼 수 있다.

https://blog.kakaocdn.net/dn/evQ0rv/btqS3H7xbZX/rWEJ7BBaUGRlkKrkG9MuyK/img.png

스택의 모습

https://blog.kakaocdn.net/dn/ISHzh/btqSTItCl1V/ntnZlqRZbex9roPON36bQk/img.png

일반 재귀의 의인화

꼬리 재귀 방식

그럼 이제 꼬리 재귀 방식을 사용해서 팩토리얼 연산을 한번 해보자. 앞서 꼬리 재귀는 '재귀 호출이 끝나면 아무 일도 하지 않고 결과만 바로 반환되도록 하는' 방법이라고 언급했다. 코드의 마지막 부분을 보면, 일반적인 재귀에서는 n * factorial(n-1) 를 반환했던 것과는 달리 곱하기 같은 연산 없이 factorial(n - 1, n * total)이라는 값만 반환하는 것을 볼 수 있다. 꼬리 재귀 방식에서는 total이라는 이미 곱셈 연산을 마친 값을 매개변수로 두었기 때문이다.

functionfactorial(n, total = 1){
if(n === 1){
return total;
    }
return factorial(n - 1, n * total);
}

위의 코드를 실행시켜보면 스택에는 아래처럼 함수들이 쌓이게 된다. 언뜻보면 비슷해보일 수 있지만 가장 큰 차이는 값들을 반환할 때 나타난다. 그림을 보면 가장 마지막으로 계산된 total 값인 6이 그대로 전달되는 걸 볼 수 있다. 각 함수들은 별도의 연산 없이, 정말 아무것도 안하고 이 값을 전달만 하는 것이다. 위에서 봤던 일반 재귀는 값을 받으면, 그 값에 연산을 하고 다른 함수에게 전달을 해줬었다. 하지만 꼬리재귀는 '아무것도 하지 않고' 값을 전달한다.

https://blog.kakaocdn.net/dn/b16b06/btqSXjUpO8I/L0LVlud5cNRNFSLVcCPJX0/img.png

스택의 모습

https://blog.kakaocdn.net/dn/5lSPw/btqSTJzm2ML/LGY8UmInnpSTEIygXXOKpk/img.png

꼬리 재귀의 의인화

다시 한 번 정리해보자. 일반 재귀의 마지막 부분은 아래처럼 생겼었다. factorial(n-1)이 바로 호출당한 함수인데, 이 함수는 스택에 쌓였다가 빠져 나올 때 원래 자기 자리로 돌아가서 앞쪽의 n과 곱해져야 한다. 다시말해 '자기 자리'를 기억하고 있어야 한다는 뜻이다. 위에서 재귀에 대해 설명할 때 스택에 입력값(매개변수), 결과값(리턴값), 그리고 리턴 후 돌아갈 위치 등이 저장된다고 언급했는데, 이 중 리턴 후 돌아갈 위치값이 바로 여기서 말하는 '자기 자리'이다.

return n * factorial(n-1);

하지만 꼬리 재귀의 마지막 부분을 보면 따로 하는 일(연산 등..)이 없기 때문에 굳이 자기 자리를 기억하지 않아도 된다. 그래서 스택에 리턴 후 돌아갈 위치값을 저장할 필요도 없어진다.