포인터(pointer)는 프로그래밍 언어에서 다른 변수, 혹은 그 변수의 메모리 공간주소를 가리키는 변수
변수는 대충 값을 저장하는 곳 같은데 메모리 공간 주소는 뭘까?
간단한 c언어 표준 입출력 코드로 알아보자
메모리 공간 주소란?
#include <stdio.h>
int main()
{
int a; // 변수 a를 정수형(int)으로 선언
printf("상수 하나를 입력해주세요: ");
scanf("%d", &a); // 사용자가 입력한 정수를 변수 a에 저장 (a의 주소에 값을 저장)
printf("입력된 값: %d\n", a); // 변수 a에 저장된 값을 출력
return 0;
}
변수를 선언하게 되면 컴파일러는 해당 변수의 자료형에 맞는 메모리 공간의 크기를 계산해주고, 운영체제는 해당 코드를 실행할 경우 계산된 메모리 크기에 맞는 메모리 공간을 확보해준다. 그래서 메모리 주소는 그때 그때 달라진다.
쉽게 말하면 선언한 변수에 해당하는만큼 메모리 공간이 확보가 된다!
int 자료형의 크기는 4 bytes (환경마다 다를 수는 있겠지만 대부분의 환경에서) 자료형을 간단히 설명하면 컴퓨터는 0과 1로 데이터를 저장하게 되는데 이를 정보의 최소 단위인 bit라고 한다. 따라서 1bit가 나타낼 수 있는 경우의 수는 2가지이다. 1byte는 8bit로 구성되어 있으므로 1byte가 나타낼수 있는 경우의 수는 2 ^ 8인 64가지이다. 따라서 int 자료형인 4 bytes는 2^32가지 경우의 수를 나타낼 수 있는데,
int는 -범위부터 정수를 나타내므로
int가 저장할 수 있는 수의 범위는 (-2^31) ~ (2^31 - 1)까지 범위의 수를 나타낼 수 있다! (양수에 -1이 된 이유는 0을 포함해서 1개가 빠짐)
쉽게 말하면 int 자료형은 4칸만큼의 메모리 공간이 확보가 된다!
&연산자는 단항연산자로 사용될 경우 주소 연산자 (Address-of Operator)로 해당 변수의 주소를 가르키게 된다.
scanf("%d", &a); 따라서 해당 코드는 a의 주소에 %d (십진수)의 형태로 입력을 받겠다는 코드이다.
해당 코드를 실행하고 1를 입력해보자
변수 a에 운영체제와 컴파일러가 0x00000000AC08FF694 라는 주소값을 할당해줬고,
자료형이 int라서 4칸 (4 byte)를 확보한 것을 확인할 수 있다.
그렇다면 이론상으로 int 자료형이 저장할 수 있는 범위인 (2^31 - 1) 인 2147483647을 대입해보자.
먼저 2147483647를 16진수로 표현해보자.
계산기로 확인해본 결과 2147483647는 16진수로 7FFF FFFF이다.
ff ff ff 7f 형태로 저장되어 있는 것을 확인할 수 있다.
이는 메모리에 리틀 엔디안 방식을 저장하고 있음을 확인할 수 있다.
리틀 엔디안: 바이트 단위로 가장 작은 바이트(LSB)를 먼저 저장하는 방식
메모리 공간 주소를 저장하는 포인터
자 그러면 본격적으로 포인터를 사용해보자.
아까 & 연산자는 a의 주소를 나타내는데 사용하였다.
포인터를 사용하여 직접 a의 주소를 포인터에 할당해보자
#include <stdio.h>
int main()
{
int a; // 변수 a를 정수형(int)으로 선언
int* p = &a; // 포인터
printf("상수 하나를 입력해주세요: ");
scanf("%d", p); // 사용자가 입력한 정수를 변수 a에 저장 (a의 주소에 값을 저장)
printf("입력된 값: %d\n", a); // 변수 a에 저장된 값을 출력
printf("입력된 값: %d\n", *p); // 포인터 p가 가리키고 있는 값을 출력
printf("코드 끝\n");
return 0;
}
1을 입력한 결과
포인터인 p가 a의 주소를 가리켜서 a에 값을 저장한다.
*는 포인터 역참조 연산자( Dereference Operator )로 포인터가 가리키고 있는 값을 정상적으로 가리키고 있는 것을 확인할 수 있다. 역참조할 때는 어떤 타입의 포인터인지가 중요하다. 뒤에 설명하겠다.
포인터에 타입이 있는 이유
그러면 int의 배열로 선언해보자!
자 이제부터 머리가 슬슬 아프다.
#include <stdio.h>
int main()
{
int a[3]; // int 3개 크기의 배열 선언
int* p = &a; // 포인터
printf("첫번째 숫자를 입력해주세요: ");
scanf("%d", p); // 사용자가 입력한 정수를 변수 a에 저장 (a의 주소에 값을 저장)
printf("두번째 숫자를 입력해주세요: ");
scanf("%d", p+1); // 사용자가 입력한 정수를 변수 a에 저장 (a의 주소에 값을 저장)
printf("입력된 값: %d\n", *p); // 변수 a에 저장된 값을 출력
printf("입력된 값: %d\n", *(p+1)); // 포인터 p가 가리키고 있는 값을 출력
printf("코드 끝\n");
return 0;
}
int 3개 크기의 배열을 선언하고 a의 주소값을 int형의 포인터 p에 할당했다.
해당 코드를 실행하고 1과 2를 입력해보았다.
위 내용을 그림으로 표현하면 아래와 같다
꼬아보기
자, 그러면 여기서 조금 더 꼬아서 p의 포인터형을 char*로 바꾸면 어떻게 될까?
아래 상황을 미리 설정하고 해당 상황을 코드로 작성해보자.
주소값에 순서대로 23, 34, 45, 56를 입력하려면 어떡해야할까? 리틀 앤디안을 고려해서 16진법으로 56453423에 해당하는 수를 찾아서 입력하면 된다.
계산기로 16진법으로 56453423 에 해당하는 수는 십진법으로 1447375907임을 확인할 수 있다.
scanf 를 받을 때 %x로 입력을 받으면 16진법으로 바로 입력 받을 수 있다.
리틀 앤디안을 고려해서 순서를 고려해야 한다.
코드 작성
#include <stdio.h>
int main()
{
int a[3]; // int 3개 크기의 배열 선언
char* p = &a; // 포인터
printf("16진법으로 56453423에 해당하는 수를 입력해주세요: "); // 1447375907 입력
scanf("%d", a); // a를 배열로 선언했을 경우 a는 &a[0]을 의미
printf("32435465를 입력해주세요: ");
scanf("%x", &a[1]); // 16진법으로 바로 입력
printf("첫번째 주소에 입력된 값을 char로 출력: %c\n", *p); // 변수 a에 저장된 값을 출력
printf("두번째 주소에 입력된 값울 char로 출력: %c\n", *(p+1)); // 포인터 p가 가리키고 있는 값을 출력
printf("코드 끝\n");
return 0;
}
결과확인
메모리를 확인해보면 계획했던대로 값이 입력된 것을 확인할 수 있다.
16진법으로 23과 34에 해당되는 값을 확인하면
각각 '#' 과 '4' 인 것을 확인할 수 있다.
포인터의 자료형에 따라 포인터가 이동하는 크기가 달라지는 것을 확인할 수 있었다.
포인터를 선언하고 해당 포인터가 가리키는 주소에 값 할당하기
그러면 자료형 변수를 선언하는게 아니라 바로 포인터를 선언하고 해당 포인터가 가르키고 있는 주소에 값을 바로 넣을 수 있지 않을까?
#include <stdio.h>
int main()
{
int* p; // 포인터 선언
printf("상수 하나를 입력해주세요: ");
scanf("%d", p); // 해당 포인터에 값 넣기
printf("입력된 값: %d\n", *p); // 포인터가 가르키고 있는 값 출력하기
return 0;
}
해당 코드의 진행을 보면 정상적으로 진행될 것 같다. int형 포인터를 선언하고 해당 포인터가 가르키는 주소에 값을 입력 값고 그 값을 출력할 것 같다. 하지만 결과는?
초기화되지 않은 변수 p를 사용해 run-time check failure가 발생하였다.
int a; 처럼 자료형으로 선언할 경우 해당 자료형에 맞는 메모리 공간이 확보되고 유효한 주소를 갖게 된다. (값은 몰론 초기화 x)
int* p 처럼 포인터를 선언할 경우, 해당 포인터를 저장할 수 있는 메모리 공간은 확보되지만 해당 포인터는 쓰레기 주소값을 가리키고 있기 때문에 유효한 주소를 할당해줘야 한다. 이럴 경우에
1. 변수를 선언하고 해당 변수의 주소에 할당해 주거나,
2. malloc을 사용하여 동적으로 메모리 주소를 할당해줄 수 있다. (이경우 메모리 누수가 일어나지 않도록 사용하고 해제해줘야 함)
1번은 이미해봤으니 2번으로 해보자.
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = malloc(sizeof(int)); // int 크기만큼 메모리 할당
printf("상수 하나를 입력해주세요: ");
scanf("%d", p);
printf("입력된 값: %d\n", *p);
free(p); // 동적 메모리 해제
printf("동적 메모리 해제\n");
return 0;
}
실행 결과
정상적으로 포인터 p가 가르키고 있는 주소에 값이 할당된 것을 확인할 수 있다
포인터 p를 저장하고 있는 공간은 따로 있으며 해당 주소(&p)에서 가리키는 값이 p의 주소임을 확인할 수 있다.
포인터 p의 크기는 8 bytes인데 이를 통해 운영체제가 64bit임을 알 수 있다.
운영체제가 64bit라는 것은 64bit의 (8bytes)의 메모리 주소를 사용할 수 있음을 뜻한다.
참고로 32bit 운영체제는 32bit의 메모리주소를 사용할 수 있어서 메모리 용량을 2^32 bytes = 4gb까지 사용할 수 있다.
기존에 프론트엔드 쪽을 준비하고 있었는데 어느 곳에 선택과 집중을 해야할 지 살짝 혼란스러운 상황에서, 글쓰기 연습 겸 제가 작성할 수 있는 가장 생산성 있는 글이 SSAFY 11기 합격 후기인 것 같아서 작성해봅니다.
SSAFY 선발 과정과 수업 내용은 삼성에서 진행해서 그런지 무서울 정도로 아주 엄격하게 보안을 지키고 있습니다. 따라서, 실제 시험 내용이나 면접 과정은 작성할 수 없고 답변 드릴 수 없습니다! 작성된 내용은 SSAFY 홈페이지에서 확인할 수 있는 내용과 제 뇌피셜입니다 :)
배경
나의 스펙
기계공학과 졸업 및 1.5년 간의 품질 엔지니어 재직 경험
0.5 년간의 국비 파이썬 웹 개발 교육 수료 및 0.5 년간의 웹개발 독학 및 취업 준비 기간
저는 기계공학과를 졸업하고 품질 엔지니어로 재직하면서, 프로그래밍이 생산 환경을 극적으로 바꾸는 것을 경험했습니다. 예를 들면 기존 수공구인 토크 드라이버를 전자식으로 바꾸면서 볼트를 체결할 때 어느 정도의 토크로 체결했는지 데이터베이스에 저장하여 전산으로 확인 가능하게 바뀌고, 테스트 프로세스가 자동화되어 작업자들의 터칭 타임이 90분에서 20분으로 바뀌는 걸 경험했습니다. 또 아무래도 프로그래밍이라는게 개인의 역량에 의존하다보니 프로그래밍을 업으로 삼는다면 저의 전문성을 기를 수 있는 직업이라고 생각되어, 퇴사를 하고 준비하게 되었습니다.
이후 싸피 9기를 지원했다가 SW 적성진단을 조지고 떨어지고, 6개월 간 국비 지원으로 웹개발 과정을 수강하게 되었습니다. 왜 웹개발이냐고 물어본다면, 웹은 항상 누구나 사용해서 시장이 열려있을 것 같고, 제일 만만해서 웹개발 공부를 시작했습니다. 해당 과정에서 git, sql, 알고리즘, 웹개발의 전반적인 내용, Django 맛보기를 할 수 있었던, 100% 받아들일 수만 있다면 아주 유익한 과정이었습니다.
이후 6개월 동안은 Django 말고 다른 백엔드 프레임워크도 궁금해서 NodeJS 수업도 찾아서 들어보고, 주로 관심이 있던 프론트엔드의 프레임워크인 React와, JS 한계점을 보안하기 위한 TypeScript도 공부하면서, 이곳 저곳에 지원하면서 6개월 을 보냈습니다.
결국 취업은 못했습니다. 반성해보면 가장 큰 문제는 제 활동을 전체적으로 정리한 게 없는게 가장 큰 것 같습니다. 취업을 할려면 기업이 요구하는 요구사항이랑 제가 했었던 활동을 비교하면서 제 이력서에서 부족한 부분을 채워가야하는 활동을 해야하는데 작성하는 걸 계속 미루고 미뤘던 것 같아요. 2024 년도에는 임베디드 관련 기업들의 요구 사항들을 확인하고 필요한 사항들을 채워가며 기록을 해야겠습니다!
지원 동기
저는 아래 이유 때문에 싸피를 지원했습니다.
돈 ⭐⭐⭐
임베디드 배울 수 있음
임베디드를 떨어지더도 웹 개발 준비 가능
취업 컨설팅 및 채용박람회, SSAFY 전형
첫 번째는 돈입니다. 퇴사를 하고 1년이 지난 시점에서 비워져가는 통장은 부담이 되었습니다. 취준 생활을 이어가기 위해서는 싸피에서 지원되는 돈이 꼭 필요했습니다.
두 번째는 임베디드를 싸피에서 배울 수 있다는 것이었습니다. 몰론 다른 임베디드를 배울 수 있는 교육 프로그램도 있지만 대부분 다 국비입니다. 활동이 전액 지원되는 국비 교육은 단 한 번만 신청할 수 있기 때문에, 이미 국비 교육을 들은 저는 신청이 불가능했습니다. 또한, SSAFY 같이 면접을 보고 교육에 참여하는 프로그램은 어느 정도 지원자들이 능력도 있고, 관심이 있는 사람들이 몰리기 때문에 네트워크를 형성하기 좋을 것 같았습니다.
세 번째는 임베디드 분반에 떨어지더라도 하고 있었던 웹개발 공부를 지속하면 되기 때문에, 떨어져도 나는 웹 개발 운명이다 라고 받아들이면 됬습니다.
네 번째는 싸피에서는 취업 컨설팅 및 채용박람회, SSAFY 전형 등 제가 취업 시장에서 전략적으로 유리한 위치와 환경을 조성할 수 있다고 생각되었습니다.
지원 과정
SW 적성진단
SW 적성진단과 CT는 정말 중요 - 수 많은 지원자를 객관적으로 평가할 수 있는 지표
저는 개인적으로 SSAFY를 준비한다면 SW 적성진단과 CT문제를 가장 우선 순위를 두고 준비해야한다고 생각합니다. 에세이에 본인이 SSAFY에 적합한 인재인지 아무리 잘 녹여내도 가장 객관적인 점수로 평가되는 SW 적성진단과 CT에서 다른 경쟁자보다 낮은 점수를 받는다면 통과가 거의 불가능하다고 생각합니다. SSAFY 경쟁률은 정확히 알려진 바가 없지만 10:1 로 가정해도 11기 기준 1150명의 합격자를 뽑기 위해서는 만 개가 넘는 에세이를 평가해야하는데, 각자 사연이 있는 에세이를 어떤 기준에 의해 평가하더라도 적당히 싸피의 인재상과 맞는다면 합격에 극적인 영향을 주기는 힘들다고 생각합니다. SW 적성진단과 CT에 비중을 두고 준비하는 것이 좋을 것 같습니다. SW 적성진단과 CT를 통과 해야 인터뷰를 볼수 있으니까요!
저는 SW 적성진단은 해커스 파랭이 수리논리랑 추리 영역을 푸는 것으로 대비했습니다. 시중에 있는 SSAFY전용 대비책은 난이도가 너무 낮다고 생각합니다. SSAFY 시험이 GSAT보다는 시간이 널널하다고 하지만 본인이 GSAT 특화형인간이 아니라면 그래도 시간이 부족할 것이라고 생각되므로 연습할 때도 실전처럼 시간에 신경쓰면서 연습하는 것을 추천드립니다.
저는 평소에 알고리즘 문제를 풀고 있었어서 CT는 따로 준비를 하지 않았습니다. 프로그래밍이 처음인데 준비해야하는 입장인데 시간적 여유가 있으신 분은 백준사이트/ 단계별로 풀어보기에서 문자열까지 파이썬으로 풀어보는 것을 추천드립니다. 시간이 없으신 분은 선배 기수 중에서 CT 문제를 잘 정리해둔 문제집을 카카오톡 오픈 단톡방이나 웹에서 찾아서 풀어보는 것을 추천드립니다.
에세이
SSAFY가 나에게 왜 필요한지 적는 곳, 자기 자랑을 적는 곳이 아니다
가장 자연스러운 흐름은
나는 목표(A)하기 위해서 노력(B)을 했다
노력(B)를 하는 도중 어려움(C)이 생겼다
SSAFY에서 활동(D)를 한다면 어려움(C)을 해결하여 목표(A)를 이룰 수 있을 것 같다.
당연히 에세이는 면접을 고려해서 작성
에세이는 매 기수 작성하는 내용이 다를 수 있지만, 결국 작성해야하는 내용은 "왜 SSAFY가 나에게 필요한가?"에 초점을 맞춰서 작성되어야 합니다. 보통 작성란 글자수가 엄청 타이트하게 제한되어 있어서 핵심만 작성하고 넘어가야 합니다. 그래서 노력(B) 부분을 작성하다가 너무 심취해서 글자수가 넘어가는 것을 경계해야 합니다.
먼저, SSAFY 홈페이지에 나와있는 소개 제일 위를 보면 "삼성 청년 SW 아카데미(SSAFY)는삼성의 SW 교육 경험과 고용노동부의 취업지원 노하우를 바탕으로 취업 준비생에게 SW 역량 향상 교육 및 다양한 취업지원 서비스를 제공하여 취업에 성공하도록 돕는 프로그램입니다." 라고 친절하게 나와 있습니다. 결국 목표(A)는 취업(or SW에 대한 관심 및 목표), 그리고, 노력(B)는 취업을 하기 위한 노력(or 수행한 SW 관련 활동), 어려움(C)는 취업 어려움(or 수행한 SW 관련 활동의 한계), 활동(D)는 SSAFY에서 하는 활동이 되게 됩니다.
저 같은 경우는 나의 스펙 부분에 적은 내용을 바탕으로 목표(A), 노력(B), 어려움(C) 그리고 제 지원 동기 부분에서 어려움(C), 활동(D)를 녹여서 작성하였습니다.
비전공자인데 SW 관련 활동이 없고 정말 뭘 적어야할지 모르겠다면 , 본인 전공 관련 프로젝트에서 SW을 융합하여 어떤 식으로 발전시킬 수 있겠다라는 방향으로 작성할 수도 있을 것 같습니다. 그것마저 없다면 지금 시간이 있다면 다 접고 코딩 공부를 직접 해보시길 바랍니다. 거기에서 찾아온 어려움을 녹여낼 수 있을 것 같습니다. 뭘 적어야할지도 모르겠고 지금 시간도 없다면... 정말 본인에게 SSAFY가 필요한지 질문해보는게 좋을 것 같습니다.
면접 때 작성한 에세이를 바탕으로 면접관이 질문할 수 밖에 없으므로, 본인이 작성한 내용에 대해서 어떤 식으로 추가 설명할 수 있을지 고민도 해보는 것이 좋을 것 같습니다.
인터뷰
얼마나 SW (IT)에 관심을 가지고 있는지?
본인의 생각을 얼마나 논리 정연하게 말할 수 있는지?
에세이 작성 내용 검증
인터뷰를 통해서 SW와 IT에 대한 관심을 확인하는 것 같습니다. 인터넷에 잘 정리되어 있는 IT기술 용어에 대한 본인만의 간단한 정의 정도는 정리해두는 것이 필요합니다. 여기에 추가로 해당 IT기술이 어떤 문제를 해결하기 위해 도입이 되었는지 본인만의 추론과 고민이 있다면 베스트입니다.
IT 지식만 물어본다면 테스트로 보면 그만인데 인터뷰로 굳이 보는 이유는 얼마나 논리적으로 설명할 수 있는지 여부를 판단하기 위해서 일 것입니다. PT면접에서 나오는 질문은 정답이 있는 질문이 아닙니다. 정답을 말해야한다는 압박에 시달리기 보다는 나의 의견을 낸다고 생각하는게 좋을 것 같습니다. 문제에 대한 정답에 초점을 맞추기 보다는 내가 제시한 방안이 내가 제시하는 근거와 일치하는가에 대한 논리만 신경써서 답변하면 충분하다고 생각합니다. 유튜브에 PT 면접 강의에서 제시하는 프레임에 맞춰서 접근하면 해당 논리가 맞춰지므로, PT 면접 강의 영상에서 제시하는 프레임에 맞춰서 답변하는 것을 연습하는 것을 추천드립니다.
저는 SSAFY 면접 전날에 취업 최종 면접에서 떨어진 것을 확인하여 멘탈이 부서진 상태로 준비할 시간도 하루 밖에 없어 스터디 없이 혼자서 준비하였습니다. 다행히 저에게 주어진 주제가 최종 면접에서 떨어진 회사를 준비하면서 준비했던 주제가 나왔습니다... 좋아해야할 지 싫어해야할 지...
면접은 저도 제가 준비를 별로 못한게 찔려서인지 많이 떨고 어버버하고 멍청하게 대답한거 같지만 그래도 계속 논리에 맞게 말할려고 면접 중 스스로 계속 되세겼습니다.
"A closure is the combination of a function and the lexical environment within which that function was declared"
"클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다"
// innerFunc 함수가 outerFunc 함수의 내부에서 정의된 중첩 함수가 아니라면
// innerFunc 함수를 outerFunc 함수의 내부에서 호출하더라라도 outerFunc 함수의 변수에 접근 불가
const x = 1;
function outerFunc() {
const x = 10;
innerFunc();
}
function innerFunc() {
console.log(x); // 1; innerFunc은 전역에서 정의되었기 때문
}
outerFunc();
렉시컬 스코프
자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디서 정의했는지에 따라 상위 스코프를 결정
스코프의 실체는 실행 컨텍스트의 렉시컬 환경. 렉시컬 환경은 자신의 외부 렉시컬 환경에 대한 참조 Outer Lexical Environment Reference를 통해 상위 렉시컬 환경과 연결. 이것이 스코프 체인
렉시컬 스코프: 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값. 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정
const x = 1;
function foo() {
const x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 결과는?
bar(); // 결과는?
함수 객체의 내부 슬롯 [[Environment]]
함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장
해당 함수가 호출되었을 때 생성될 함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장될 참조 값
외부 렉시컬 환경에 대한 참조에는 함수 객체의 내부 슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조가 할당 됨
클로저와 렉시컬 환경
외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조 가능. 이러한 중첩 함수를 클로저 closure라고 부름
실행 컨텍스트가 실행 컨텍스트 스택에서 제거되어도 다른 곳에서 참조되어지고 있다면 해당 렉시컬 환경은 소멸하지 않음
클로저는 중첩 함수가 상위 스코프 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적 (자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로는 모든 함수는 클로저)
자유 변수 free variable: 클로저에 의해 참조되는 상위 스코프의 변수, 클로저는 함수가 자유 변수에 닫혀있다라는 의미
const x = 1;
function outer() {
const x = 10;
const inner = function () {
console.log(x);
};
return inner;
}
// outer 함수를 호출하면 중첩함수 inner를 반환
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텐스트에서 팝되어 제거됨
const innerFunc = outer();
innerFunc(); // 10
클로저의 활용
클로저는 상태 state를 안전하게 변경하고 유지하기 위해 사용. 상태를 은닉 information hiding , 특정 함수에게만 상태 변경을 허용하게 함
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가시킴
return ++num;
};
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
// 오류 가능성
// 1. 카운트 상태(num변수의 값)는 increase 함수가 호출되기 전까지 변경되지 않고 유지되어야 함
// 2. 이를 위해 카운트 상태(num변수의 값)는 increase 함수만이 변경할 수 있어야 함
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태 변수
let num = 0;
// 카운트 상태를 1만큼 증가시킴
return ++num;
};
// 이전 상태를 유지하지 못함 - 함수가 호출될 때마다 새로운 렉시컬 환경 생성
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
// 카운트 상태 변경 함수
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
return ++num;
};
})();
// 카운트 증가 성공
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
const counter = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저인 메서드를 갖는 객체를 반환
// 객체 리터럴은 스코프를 만들지 않음
// 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경
return {
// num:0 - 프로퍼티는 public하므로 은닉되지 않음
increase() {
return ++num;
},
decrease() {
return num > 0 ? --num : 0;
},
};
})();
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
console.log(counter.decrease()); // 0
// 위 코드를 생성자 함수로 표현할 경우
const Counter = (function () {
// 카운트 상태 변수
let num = 0;
function Counter() {
// this.num = 0 - 프로퍼티는 public하므로 은닉되지 않음
}
Counter.prototype.increase = function () {
return ++num;
};
Counter.prototype.decrease = function () {
return num > 0 ? --num : 0;
};
return Counter;
})();
const counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
console.log(counter.decrease()); // 0
// 함수를 인수로 전달받고 함수를 반환하는 함수
// 이 함수는 카운트 상태를 유지하기 위한 counter를 기억하는 클로저를 반환
function makeCounter(aux) {
// 카운트 상태를 유지하기 위한 자유 변수
let counter = 0;
// 클로저 반환
return function () {
// 인수로 전달받은 보조 함수에 상태 변경을 위임
counter = aux(counter);
return counter;
};
}
// 보조 함수
function increase(n) {
return ++n;
}
function decrease(n) {
return --n;
}
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2
// makeCounter 함수를 호출해 함수를 반환할 때 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖음
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
// 독립된 카운터가 아니라 연동하여 증감이 가능한 카운터를 만들려면 렉시컬 환경을 공유하는 클로저를 만들어야 함
// 이를 위해서는 makeCounter함수를 두 번 호출하지 말아야 함
// 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환
const counter = (function () {
// 카운트 상태를 유지하기 위한 자유 변수
let counter = 0;
// 함수를 인수로 전달 받는 클로저를 반환
return function (aux) {
// 인수로 전달받은 보조 함수에 상태 변경을 위임
counter = aux(counter);
return counter;
};
})();
// 보조 함수
function increase(n) {
return ++n;
}
function decrease(n) {
return --n;
}
// 보조 함수를 전달하여 호출
console.log(counter(increase)); // 1
console.log(counter(increase)); // 2
// 자유 변수 공유
console.log(counter(decrease)); // 1
console.log(counter(decrease)); // 0
캡슐화와 정보 은닉
캡슐화 encapsulation는 객체의 상태 state를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작 behavior인 메서드를 하나로 묶는 것. 이를 통해 객체의 특성 프로퍼티나 메서드를 감추는 목적(정보 은닉 information hiding)으로 사용하기도 함
정보 보호 및 객체 간의 상호 의존성 (결합도 coupling)을 낮추는 효과가 있음
대부분의 객체지향 프로그래밍 언어는 public, private, protectedt 같은 접근 제한자 access modifier를 선언하여 공개 범위를 한정 가능
자바스크립트는 이제 private를 필드를 정의할 수 있는 새로운 사양을 도입하려는 듯? 기본적으로는 지원하지 않음
흉내내는 방법은 한계가있어 근본적인 해결은 아님
function Person(name, age) {
this.name = name; // public
let _age = age; // private
// 인스턴스 메서드
this.sayHi = function () {
console.log(`Hi! my name is ${this.name}. I am ${_age}`);
};
}
const me = new Person("Jin", 29);
me.sayHi(); // Hi! my name is Jin. I am 29
console.log(me.name); // Jin
console.log(me._age); // undefined
// sayHi 메서드는 인스턴스 메서드이므로 Person 객체가 생성될 때마다 중복 생성됨
function Person(name, age) {
this.name = name; // public
let _age = age; // private
}
// 프로토타입 메서드
Person.prototype.sayHi = function() {
// Person 생성자 함수의 지역 변수 _age를 참조할 수 없음
console.log(`Hi! my name is ${this.name}. I am ${_age}`)
}
// 즉시 실행 함수를 사용하여 Person 생성자 함수와 prototype 메서드를 하나의 함수에 모아보자
const Person = (function () {
let _age = 0;
// 생성자 함수
function Person(name, age) {
this.name = name; // public
_age = age;
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log(`Hi! my name is ${this.name}. I am ${_age}`);
};
// 생성자 함수 반환
return Person;
})();
const me = new Person("Jin", 29);
me.sayHi(); // Hi! my name is Jin. I am 29
console.log(me.name); // Jin
console.log(me._age); // undefined
const you = new Person("Park", 20);
you.sayHi(); // Hi! my name is Park. I am 20
console.log(you.name); // Park
console.log(you._age); // undefined
// 문제점
me.sayHi(); // Hi! my name is Jin. I am 20
// Person.prototype.sayHi 메서드가 단 한번 생성되는 클로저
// Person.prototype.sayHi 메서드는 모든 인스턴스가 하나의 동일한 상위 스코프를 사용
자주 발생하는 실수
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () {
return i;
};
}
for (var j = 0; j < 3; j++) {
console.log(funcs[j]());
}
// 3
// 3
// 3
// var는 블록 레벨 스코프가 아닌 함수 레벨 스코프를 갖기 때문에 전역 변수
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
return i;
};
}
for (var j = 0; j < 3; j++) {
console.log(funcs[j]());
}
// 0
// 1
// 2
// let은 블록 레벨 스코프를 가져, 매 반복마다 렉시컬 스코프를 생성
// 고차 함수와 화살표 함수를 사용한 방법
const funcs = Array.from(new Array(3), (_, i) => () => i);
funcs.forEach((f) => console.log(f()));
// 0
// 1
// 2
ECMAScript 사양은 소스코드를 4가지 타입으로 구분. 4가지 타입의 소스코드는 실행 컨텍스트를 생성
소스코드의 타입에 따라 실행 컨텍스트르 생성하는 과정과 관리 내용이 다르기 때문
소스코드의 타입
설명
전역 코드 global code
- 전역에 존재하는 소스코드. 전역에 정의된 함수, 클래스 등의 내부 코드는 포함되지 않음 - 전역 변수를 관리하기 위해 최상위 스코프인 전역 스코프 생성 - var키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수를 전역 개체의 프로퍼티와 메서드로 바인딩하고 참조하기 위해 전역 변수와 연결 - 전역 코드가 평가되면 전역 실행 컨텍스트 생성
함수 코드 function code
- 함수 내부에 존재하는 소스코드. 함수 내부에 중첩된 함수, 클래스 등의 내부 코드는 포함되지 않음 - 지역 스코프를 생성하고 지역변수, 매개변수, arguments 객체를 관리 - 생성한 지역 스코프를 전역 스코프에서 시작하는 스코프 체인의 일원으로 연결 - 함수 코드가 평가되면 함수 실행 컨텍스트 생성
eval 코드 eval code
- 빌트인 전역 함수인 eval 함수에 인수로 전달되어 실행되는 코드 - strict mode에서 자신만의 독자적인 스코프 생성 - eval코드가 평가되면 eval 컨텍스트가 생성
모듈 코드 module code
- 모듈 내부에 존재하는 소스코드. 모듈 내부의 함수, 클래스 등의 내부 코드는 포함되지 않음 - 모듈별로 독립적인 스코프 생성 - 모듈 코드가 평가되면 모듈 실행 컨텍스트 생성
소스코드의 평가와 실행
자바스크립트는 "소스코드의 평가"와 "소스코드의 실행" 과정으로 나누어 처리
소스코드의 평가: 실행 컨텍스트를 생성하고 변수, 함수 등의 선언문만 먼저 실행하여 생성된 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프 (렉시컬 환경의 환경 레코드)에 등록
소스코드의 실행: 선언문을 제외한 소스코드가 순차적으로 실행 (런타임). 소스코드 실행에 필요한 정보, 즉 변수나 함수의 참조를 실행 컨텍스트가 관리하는 스코프에서 검색하여 취득. 변수 값의 변경 등 소스코드의 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프에 등록
실행 컨텍스트의 역할
실행 컨텍스트는 소스코드를 실행하는데 필요한 환경을 제공하고 실행 결과를 실제로 관리하는 영역
선언에 의해 생성된 모든 식별자(변수, 함수, 클래스 등)를 스코프를 구분하여 등록하고 상태 변화(식별자에 바인딩된 값의 변화)를 지속적으로 관리해야 함
스코프는 중첩 관계에 의해 스코프 체인을 형성해야 함. 즉 스코프 체인을 통해 상위 스코프로 이동하며 식별자를 검색할 수 있어야 함
현재 실행 중인 코드의 실행 순서를 변경(함수의 호출에 의한 변경 등)할 수 있어야 하며 다시 되돌아갈 수도 있어야 함
실행 컨텍스트는 식별자(변수, 함수, 클래스 등의 이름)를 등록하고 관리하는 스코프와 코드 실행 순서를 관리를 구현한 내부 메커니즘. 모든 코드는 실행 컨텍스트를 통해 실행되고 관리
식별자와 스코프: 실행 컨텍스트의 렉시컬 환경
코드 실행 순서: 실행 컨텍스트 스택
실행 컨텍스트 스택
실행 컨텍스트 스택은 코드의 실행 순서를 관리
실행 컨텍스트는 스택 stack 자료 구조로 관리
실행 중인 실행 컨텍스트 running execution context: 실행 컨텍스트 스택의 최상위에 존재하는 실행 컨텍스트. 현재 실행 중인 코드의 실행 컨텍스트
const x = 1;
function foo() {
const y = 2;
function bar() {
const z = 3;
console.log(x + y + z);
}
bar();
}
foo(); // 6
렉시컬 환경
렉시컬 환경 lexical environment는 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 차몾를 기록하는 자료구조로 실행 컨텍스트르 구성하는 컴포넌트
렉시컬 환경은 스코프와 식별자를 관리
실행 컨텍스트는 LexicalEnvironment 컴포넌트와 VariableEnvironment로 구성되어 있고, 초기에는 하나의 동일한 렉시컬 환경을 참조하나, 몇 가지 상황에 달라지는 경우가 있으나 이번 정리에서는 구분하지 않음
렉시컬 환경은 두개의 컴포넌트로 구성
환경 레코드 environment record
스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소
환경 레코드는 소스코드의 타입에 따라 관리하는 내용에 차이가 있음
외부 렉시컬 환경에 대한 참조 outer lexical environment reference
외부 렉시컬 환경에 대한 참조는 상위 스코프를 가리킴
상위 스코프란 외부 렉시컬 환경, 즉 해당 실행 컨텍스트를 생성한 소스코드를 포함하는 상위 코드의 렉시컬 환경을 뜻함
외부 렉시컬 환경에 대한 참조를 통해 단방향 링크드 리스트인 스코프 체인을 구현
실행 컨텍스트의 생성과 식별자 검색 과정
var x = 1;
const y = 2;
function foo(a) {
var x = 3;
const y = 4;
function bar(b) {
const z = 5;
console.log(a + b + x + y + z);
}
bar(10);
}
foo(20); // 42
전역 객체 생성
전역 코드 평가
전역 실행 컨텍스트 생성
전역 렉시컬 환경 생성
전역 환경 레코드 생성
객체 환경 레코드 생성
선언적 환경 레코드 생성
this 바인딩
외부 렉시컬 환경에 대한 참조 결정
전역코드 실행
식별자 결정 indentifier resolution: 어느 스코프의 식별자를 참조하면 되는지 결정, 실행 중인 실행 컨텍스트에서 식별자 검색 시작
foo 함수 코드 평가
함수 실행 컨텍스트 생성
함수 렉시컬 환경 생성
함수 환경 레코드 생성
this 바인딩
외부 렉시컬 환경에 대한 참조 결정
함수를 어디서 호출했는지가 아니라 어디에 정의했는지에 따라 상위 스코프 결정
foo 함수 코드 실행
식별자 결정을 위해 실행 컨텍스트의 렉시컬 환경에서 식별자를 검색하기 시작
만약 실행 중인 실행 컨텍스트의 렉시컬 환경에서 식별자를 검색할 수 없으면 외부 렉시컬 환경에 대한 참조가 가리키는 렉시컬 환경으로 이동하여 식별자를 검색
bar 함수 코드 평가
foo 코드 평가와 같은 실행 컨텍스트와 렉시컬 환경의 생성 과정 진행
bar 함수 코드 실행
console 식별자 검색
log 메서드 검색
표현식 a + b + x + y + z의 평가
현재 실행 중인 실행 컨텍스트의 렉시컬 환경에서 시작하여 외부 렉시컬 환경에 대한 참조로 이어지는 렉시컬 환경의 연속에서 검색
console.log 메서드 호출
bar 함수 코드 실행 종료
foo 함수 코드 실행 종료
전역 코드 실행 종료
실행 컨텍스트 스택에는 아무것도 남지 않게 됨
실행 컨텍스트와 블록 레벨 스코프
let, const 키워드로 선언한 변수는 모든 코드 블록(함수, if 문, for 문, while문, try/catch 문 등)을 지역 스코프로 인정하는 블록 레벨 스코프 block-level scope를 따름
해당 코드 블록이 실행되면 해당 코드 블록을 위한 블록 레벨 스코프가 생성하고, 선언적 환경 레코드를 갖는 렉시컬 환경을 새롭게 생성하여 기존의 전역 렉시컬 환경을 교체
이 때 새롭게 생성된 코드 블록을 위한 외부 렉시컬 환경의 외부 렉시컬 환경에 대한 참조는 해당 코드 블록이 실행되기 이전의 전역 렉시컬 환경을 가리킴
참고: var 키워드로 선언한 변수는 오로지 함수의 코드 블록만 지역 스코프로 인정하는 함수 레벨 스코프를 따름
let x = 1;
if (true) {
let x = 10;
console.log(x); // 10
}
console.log(x); // 1