(19)

JavaScript 객체는 해시 테이블이 아닌가? – V8의 Hidden Class와 Inline Caching

최근 면접 이야기최근 기술면접에서 다음과 같은 질문을 받았다. "JS에서 Array에 모든 데이터를 Array에 넣고, find()로 찾으면 되지, 왜 굳이 객체를 사용할까요?" 이 질문에 대해 "find()는 O(N)이지만, 객체는 프로퍼티를 통해 값을 조회할 수 있어서 O(1)이기 때문에 사용한다고 생각합니다." 라고 대답했고, 다음과 같은 꼬리질문들이 이어지기 시작했다.객체는 어떻게 값을 저장하길래 O(1)인가요?객체는 값을 가져올 때 항상 O(1)인가요? 정말인가요? 최근 학습했던 JS와 V8의 메모리 구조와 관리 내용을 기반으로 알고 있던 지식을 버무려 답하고자 했다. 생각하는 시간을 가질수록 머리가 하얘져서, 객체의 프로퍼티 - 값 형태에 집착했고, 해시 구조로 저장된다는 답을 드렸다. 그 ..

JavaScript의 메모리 구조와 관리, V8의 가비지 컬렉션 (스택이 무한정 커지면 힙은 불필요할까?)

서론 출근길 최고의 선택 중 하나인 널개님의 CS 영상을 보면서 출근을 했다. 오늘 영상의 주제는 다음과 같았다.스택이 무한정 커졌다고 가정할 때, 힙은 불필요할까?힙의 파편화에 대해 알고있나? 유튜브를 보는 내내 30분 동안 지하철에서 머리속에 흩어져 있는 지식을 조합해서 대답을 만들어보았다. 일반적으로 메모리 공간은 스택, 힙, 코드, 데이터가 어쩌고.....JavaScript의 실행 컨텍스트가 스택으로 관리되고 내부적으로는 동적으로 생성되고 가비지 컬렉션이 어쩌고......Primitives는 일반적으로 스택에 저장되고.......힙 파편화는 메모리 할당, 해제가 반복되면서 어쩌고.... 디스크 조각모음 어쩌고.... 힙 파편화가 아니라, 내 머리 속의 파편부터 GC하고 싶은 출근길이었다.. ..

[JavaScript] 식사 메뉴를 선정을 위한 돌림판(룰렛) 만들기

결정장애라 메뉴선정시에 항상 어려움을 겪곤 한다. 학원 수강 중 메뉴 선정에 대한 고민을 하였고, 결국 오픈소스를 활용하여 돌림판 제작을 시작했다. See the Pen Untitled by magic (@mag11c) on CodePen. 자주가는 식당들을 product 배열에 미리 추가한 뒤, 해당 배열의 길이와 동일하게 색상 배열을 만들어 색상을 미리 추가시켰다. 그리고 새로운 메뉴를 추가할 수 있다. const $c = document.querySelector("canvas"); const ctx = $c.getContext(`2d`); const menuAdd = document.querySelector('#menuAdd'); const product = ["햄버거", "순대국", "정식당", ..

[JavaScript] localStorage를 활용한 todoList 만들기

HTML todos Left click to toggle completed. Right click to delete todo input에서 입력된 값을 ul의 li에다가 담을 것이며, 스토리지를 이용해 새로고침해도 리스트가 남아있게 할 예정이다. 자바스크립트 공부를 위함이니 css는 맨 아래에다가 작성 해 두겠다. JS 새로고침 이후에도 유지되기 위해 스토리지를 활용할 것이다. localStorage에 데이터를 넘겨주기 위해 배열을 JSON.stringify() 메서드로 포맷팅한 뒤 넘겨주었다. const form = document.querySelector('form'); const input = document.querySelector('input'); const ul = document.queryS..

[JavaScript] 웹 스토리지(Web Storage)

웹 스토리지 ( Web Storage ) 기존 쿠키의 문제점을 보완하기 위해 사용한다. https://mag1c.tistory.com/187 쿠키와 세션 (Cookie & Session) HTTP 프로토콜의 특징 비연결성 ( Connectionless ) 클라이언트가 서버에 요청(Request)할 때, 그에 대한 응답(Response)을 한 후, 연결을 끊는다. 비상태성 ( Stateless ) 클라이언트의 상태 정보를 가지지 않는 mag1c.tistory.com 웹 스토리지의 특징 1. 브라우저 내부에 Key, Value 쌍을 저장하는 공간이다. 2. 네트워크 요청 시 서버로 전송되지 쿠키에 비해 않아 많은 자료보관이 가능하다. (2MB이상) 3. 서버가 HTTP 헤더를 통해 스토리지 객체를 조작할 수..

[JavaScript] 쿠키(Cookie) 사용하기

쿠키에 대한 자세한 설명은 아래에 https://mag1c.tistory.com/187 쿠키와 세션 (Cookie & Session) HTTP 프로토콜의 특징 비연결성 ( Connectionless ) 클라이언트가 서버에 요청(Request)할 때, 그에 대한 응답(Response)을 한 후, 연결을 끊는다. 비상태성 ( Stateless ) 클라이언트의 상태 정보를 가지지 않는 mag1c.tistory.com 자바스크립트에서의 Cookie 사용하기 document.cookie 프로퍼티를 이용하여 생성, 삭제 및 조회가 가능하다. document.cookie는 name=value 쌍으로 구성되어 있고, 각 쌍은 ; 로 구분한다. 쌍 하나는 하나의 독립된 쿠키를 나타내며, ; 을 기준으로 document...

[JavaScript] async await

Promise사용 시 불편한 점들 1. then()을 연속적으로 호출하여 문제 발생 시 혼란을 겪을 수 있다. 2. 복잡한 구조의 비동기처리 코드 작성 시 코드의 가독성이 저하된다. 3. 동기코드와 비동기코드가 섞여있을 때 예외처리 하기가 난해하다. async / await Promise의 불편한 점들을 해결하기 위해 ES7에서 async/await가 추가되었다. async function f() { return 1; } f().then(alert); // 1 async는 function 앞에 위치하며, async가 붙은 함수는 항상 Promise를 반환한다. Promise객체를 생성하지 않더라도 Promise 객체가 리턴되는 것에 주의하자. await는 async함수 내부에서만 동작한다. async f..

[JavaScript] Promise

프로미스(Promise) 비동기 처리를 위해 사용되는 객체이며, 콜백 패턴의 한계를 보완하기 위해 사용한다. 장점 비동기 처리 시점을 명확하게 표현 가능 연속된 비동기 처리 작업을 수행하기 편하며, 작업 상태를 쉽게 확인 가능 코드의 유지보수성이 증가. const promise = () => new Promise((resolve, reject) => { let sum = 1 + 1 if(sum == 2) { resolve('correct') } else { reject('incorrect') } }) promise().then((message) => { console.log(message) }).catch((message) => { console.log(message) }) resolve와 reject ..

[JavaScript] 콜백 함수

콜백 함수 ( Callback Function ) 함수 안에 실행하는 또 다른 함수이며 파라미터로 함수를 전달하는 함수이다. 또한 함수 이름없이 익명으로 전달이 가능한 함수를 말한다. function introduce (lastName, firstName, callback) { var fullName = lastName + firstName; callback(fullName); } function hello (name) { console.log("제 이름은 " + name + "입니다"); } function bye (name) { console.log("지금까지 " + name + "이었습니다."); } introduce("나", "지만", hello); //제 이름은 나지만입니다 introduce("..

JavaScript 객체는 해시 테이블이 아닌가? – V8의 Hidden Class와 Inline Caching

Tech/JS & TS 2025. 6. 26. 17:56
728x90
728x90

최근 면접 이야기

최근 기술면접에서 다음과 같은 질문을 받았다.

 

"JS에서 Array에 모든 데이터를 Array에 넣고, find()로 찾으면 되지, 왜 굳이 객체를 사용할까요?"

 

이 질문에 대해

 

"find()는 O(N)이지만, 객체는 프로퍼티를 통해 값을 조회할 수 있어서 O(1)이기 때문에 사용한다고 생각합니다."

 

 

라고 대답했고, 다음과 같은 꼬리질문들이 이어지기 시작했다.

  1. 객체는 어떻게 값을 저장하길래 O(1)인가요?
  2. 객체는 값을 가져올 때 항상 O(1)인가요? 정말인가요?

 

최근 학습했던 JS와 V8의 메모리 구조와 관리 내용을 기반으로 알고 있던 지식을 버무려 답하고자 했다. 생각하는 시간을 가질수록 머리가 하얘져서, 객체의 프로퍼티 - 값 형태에 집착했고, 해시 구조로 저장된다는 답을 드렸다.

 

그 이후에도 계속해서 질문들이 이어졌고, 이미 한 번 엉켜져버린 탓에 그 무엇에도 자신있게 상세한 대답을 드리지 못했다.

  1. 해시 형태로 저장된다고 하셨는데, 그럼 객체는 해시 테이블인가요?
  2. 메모리에 해시 테이블이 어떤 구조로 올라가나요?

 

면접이 끝난 후에 부정확했던 개념들에 대해, 다시 한 번 인지하기 위해 정리하는 글이다.

이 포스팅에서는 V8의 객체가 어떻게 저장되고 사용되는지에 초점을 맞춰 정리해보려고 한다.

 

 

 

JS 객체는 해시 테이블이 아니다

객체는 프로퍼티 - 값의 형태고, 직관적으로 user.name과 같은 접근은 O(1)인 것처럼 보인다. 이러한 특징들 때문에 해시 테이블인가? 라고 생각의 혼선이 생겼었다. 하지만 정확하게는 틀렸다. 

 

적어도 V8엔진 위의 자바스크립트는, Hidden Class의 Map 구조로 생성되며, Fast Property Access 방식을 통해 성능을 최적화한다. 이 구조는 클래스 기반 언어의 필드를 고정된 순서로 저장하는 방식과 유사하며, 객체의 프로퍼티들이 특정 오프셋에 정적으로 매핑되도록 최적화되어있다. 즉, 초기에는 고정된 내부 메모리 레이아웃을 따른다는 말이다.

 

 

히든 클래스

더 자세하게 이해하기 위해, 자바스크립트가 동적 타입 언어라는 것을 상기할 필요가 있다. 컴파일 단계에서 객체가 어떤 구조가 될 지 알 수없기 때문에, V8은 히든 클래스를 이용해 구조적인 패턴을 감지하고 최적화한다.

 

https://v8.dev/docs/hidden-classes

 

 

V8의 히든 클래스 설명에 따르면, Map은 히든 클래스 자체, DescriptorArray는 Map이 가진 프로퍼티 순차 리스트와 위치 정보, 마지막으로 TransitionArray는 Map에서 Map으로 전이될 때의 간선으로, 이를 통해 트랜지션 트리를 형성하고 객체 구조의 모양(shape)가 같을 경우 동일한 히든 클래스를 재사용한다.

 

이런 매커니즘을 기반으로 히든 클래스는, 동일한 구조를 가진 객체는 동일한 히든 클래스를 사용하며, 이를 위해 객체에 프로퍼티를 추가할 때 마다 다른 히든 클래스를 사용한다.

 

예제 코드를 통해 어떻게 히든 클래스가 생성되는지 알아보자.

function User(name, age) {
  this.name = name;
  this.age = age;
}
const user1 = new User("Alice", 30);

 

 

객체 생성 직후, name, age 프로퍼티 추가에 각각의 히든 클래스를 생성한다. 최초 텅 빈 히든 클래스(Map0)에서 시작해 프로퍼티가 추가되는 순서에 따라 Transition Chain을 생성한다. 이 때 DescriptorArray에는 name, age 프로퍼티가 순서대로 구성되고, 여러 Map에서 공유될 수 있다. 아래의 그림에서, Map2까지 같은 DescriptorArray를 공유하여 히든 클래스를 구성한다.

 

 

그럼 왜 이런 구조를 설계했을까? V8에서는 동적 타입 언어의 특성 때문에, 런타임에 객체가 어떤 구조일지 알 수 없기 때문에, 히든 클래스를 추적하여 어떤 구조인지 파악해나가기 위함이라고 한다. 이 히든 클래스를 통해 V8이 구조 패턴을 감지하고 최적화를 수행할 수 있다고 언급한다. 

 

https://v8.dev/docs/hidden-classes

 

 

 

만약, 같은 User을 통해 생성된 새로운 user가 다른 프로퍼티들을 갖게 된다면 어떻게 될까?

const user2 = new User("Bob", 28);
user2.gender = "M";
user2.phoneNumber = "010-0000-0000";

 

 

user를 선언하고 최초 할당하는 순간까지는 같은 구조이기 때문에 동일한 Map2 히든 클래스를 사용하게 된다.

 

하지만 프로퍼티 추가 순서가 바뀌거나 조건에 따라 다른 구조가되면, 이 둘은 다른 클래스를 가지게 된다. user2는 Map4라는 다른 히든 클래스를 가지게 되었다. 이렇게 다른 히든 클래스를 가지는 객체는 인라인 캐싱에서 효율적으로 동작하지 않는다.

 

 

실제로 객체에 많은 프로퍼티가 추가되거나 삭제되면 DescriptorArray와 히든 클래스를 유지하는데 많은 오버헤드가 발생할 수 있다.
이를 방지하기 위해 V8은 Slow Property도 지원한다. 이 Slow Property를 가진 객체는 소위 Dictionary Mode가 되어 해당 객체는 딕셔너리에 직접 저장되어 더 이상 히든 클래스를 통해 공유되지 않는다.

 

https://v8.dev/blog/fast-properties

 

 


인라인 캐시

인라인 캐시(IC)는 특정 프로퍼티 접근이 반복될 때, 해당 오프셋 정보를 캐싱하여 다시 계산하지 않도록 한다. 이를 통해 반복적인 객체 접근 시 빠른 속도를 유지할 수 있다. 객체가 호출될 때 마다 객체 참조를 통해 힙에서 직접 객체를 조회하는 것이 아닌, 미리 캐싱된 데이터를 참조하는 것이다.

 

인라인 캐시는 다음과 같은 상태를 거친다.

  1. Uninitialized: 처음 호출되는 시점. 전체 탐색을 수행해 프로퍼티를 찾고, 히든 클래스와 offset을 기록한다.
  2. Monomorphic: 항상 같은 객체가 들어올 경우, 캐싱하게 된다.
  3. Polymorphic: 2~4가지 정도의 다양한 구조가 반복될 경우
  4. Megamorphic: 5개 이상의 다양한 구조가 등장한 경우 - 최적화 불가능

위의 user1, user2를 예시로 간단하게 코드를 작성해보았다.

function logName(user) {
  console.log(user.name);
}

const u1 = { name: 'Alice' };  // Map1
const u2 = { name: 'Bob' };    // Map1 → Monomorphic 유지
const u3 = { name: 'Charlie', age: 25 };  // Map2 → Polymorphic 전환

// 첫 번째 호출: 모노모픽 상태로 최적화
logName(u1); // IC가 {name: string} 구조에 특화

// 두 번째 호출: 같은 구조이므로 최적화 유지
logName(u2); // 캐시 히트

// 세 번째 호출: 다른 구조로 인해 폴리모픽으로 전환
logName(u3); // IC가 여러 구조를 처리하도록 변경, 성능 저하

 

우리가 아는 해시 함수를 통한 해시 구조도 항상 O(1)을 보장하는 것은 아니다. 해시 충돌이나 리사이징 등 다양한 비용이 숨어있다. 객체의 프로퍼티를 통한 값의 조회 역시 항상 O(1)일 수는 없는데, 다음과 같은 대표적인 경우에 인라인 캐시의 대상에서 제외되고, Fast Property 구조는 깨진다.

 

// 1. 메가모픽 상태
function logName(user) {
  console.log(user.name);
}

// 너무 많은 다른 구조의 객체들
const users = [
  { name: 'Alice' },
  { name: 'Bob', age: 25 },
  { name: 'Charlie', age: 30, city: 'Seoul' },
  { name: 'David', age: 35, city: 'Busan', job: 'Engineer' },
  { name: 'Eve', age: 40, city: 'Daegu', job: 'Designer', hobby: 'Reading' }
  // ... 더 많은 다른 구조들
];

users.forEach(logName); // 메가모픽 상태로 전환
// IC가 포기하고 일반적인 프로퍼티 룩업 사용
// 2. 동적인 프로퍼티 변경
function processUser(user) {
  console.log(user.name);
}

const user = { name: 'Alice' };
processUser(user); // 최적화됨

// 런타임에 프로퍼티 추가
user.age = 25;        // 히든 클래스 변경
user.city = 'Seoul';  // 또 다른 히든 클래스

// delete 연산은 기존의 히든 클래스 구조와 DescriptorArray를 무효화시키고
// V8이 해당 객체를 Dictonary Mode로 전환시킨다.
delete user.age;

processUser(user); // 딕셔너리 모드로 전환

 

// 3. 너무 많은 프로퍼티
const bigObject = {};
for (let i = 0; i < 2000; i++) {
  bigObject[`prop${i}`] = i;
}

 

 

 

 

 

 

객체 접근이 느려지는 코드 패턴을 피하자

동적으로 프로퍼티를 추가하거나 객체 생성에서 프로퍼티 순서나 구조가 달라지는 코드를 지양해야한다. 가능한 동일한 객체 구조를 유지하며 동적으로 프로퍼티를 추가하지 말고, 기본 구조 안에서 undefined를 활용하는 것이 좋다.

 

이런 패턴은 자연스레 작성하게 되는데, 개발 시에 보통 도메인 객체 혹은 모델 객체로 출발하여 비즈니스 로직을 작성하고, DTO로 변환하는 로직들을 자주 작성하게 된다. 이 때 기본적인 인터페이스나 클래스를 선언하고, 정적으로 확장해서 사용하지 동적으로 프로퍼티들을 추가하지는 않는다. 아마 고대(?) 오래 전부터 사용되고 발전되어온 언어 사용의 패턴을 현재 시점을 살아가는 우리가 사용하고 있기 때문에, 세부 내용은 모르지만 자연스레 좋은 습관으로 자리잡고 있는 것으로 보인다.

 

 

 

정리

다시 돌아와서, 객체 접근이 O(1)인 이유는 해시 형태이기 때문에 아니라, 프로퍼티가 오프셋으로 정적 매핑 되어있기 때문에, 히든 클래스를 기반으로 고정된 위치(offset)를 알 수 있기 때문이다. 반면, Array또한 INDEX를 프로퍼티로 가진 객체지만, find()는 순회하기 때문에 O(N)의 시간 복잡도를 가진다. 결과적으로 V8이 해시 기반이 아닌 정적 구조로 최적화했기 때문에 객체 접근에 O(1)이 성립하는 것이다.

 

 

이 외에도, 아래 V8 레퍼런스들을 참고하면서 알게된 사실들을 정리하면서 마치려고 한다.

 

 

 

Array의 INDEX는 객체의 프로퍼티와 다른 저장소에 저장된다.

https://v8.dev/blog/fast-properties

 

배열의 프로퍼티는 Indexed Properties라는 별도 공간에 저장된다고 한다.

(일반적인 객체의 프로퍼티는 Named Properties에 저장된다.)

 

 

배열을 최적화하기 위해 V8에서 판단하는 내부 기준(?)이 있다.

 

V8은 배열의 요소(element)를 내부적으로 SMI(정수), DOUBLE(부동소수점), OBJECT(혼합형)로 나누고,

각 타입마다 PACKED(꽉 찬 배열), HOLEY(중간에 빈 구간이 존재) 여부에 따라 최적화 전략을 다르게 적용한다.

const a = [1, 2, 3]    // PACKED_SMI_ELEMENTS
const b = [1, 2.2, 3]  // PACKED_DOUBLE_ELEMENTS
const c = [1, 'a', 3]  // PACKED_ELEMENTS
const d = [1, , 3]     // HOLEY_SMI_ELEMENTS
const e = [1, , 3.3]   // HOLEY_DOUBLE_ELEMENTS
const f = [1, , 'a']   // HOLEY_ELEMENTS

 

-0, NaN, undefined등을 배열에 넣지 말고 등등의 최적화 방법에 대해 자세히 알고 싶다면 https://v8.dev/blog/elements-kinds를 참고하면 되겠다.

 

 

 

 

 

References

https://v8.dev/blog/fast-properties

https://v8.dev/docs/hidden-classes

https://v8.dev/blog/elements-kinds

https://braineanear.medium.com/the-v8-engine-series-iii-inline-caching-unlocking-javascript-performance-51cf09a64cc3

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

JavaScript의 메모리 구조와 관리, V8의 가비지 컬렉션 (스택이 무한정 커지면 힙은 불필요할까?)

Tech/JS & TS 2025. 4. 5. 17:48
728x90
728x90

서론

 

 

출근길 최고의 선택 중 하나인 널개님의 CS 영상을 보면서 출근을 했다. 오늘 영상의 주제는 다음과 같았다.

  • 스택이 무한정 커졌다고 가정할 때, 힙은 불필요할까?
  • 힙의 파편화에 대해 알고있나?

 

유튜브를 보는 내내 30분 동안 지하철에서 머리속에 흩어져 있는 지식을 조합해서 대답을 만들어보았다.

 

일반적으로 메모리 공간은 스택, 힙, 코드, 데이터가 어쩌고.....

JavaScript의 실행 컨텍스트가 스택으로 관리되고 내부적으로는 동적으로 생성되고 가비지 컬렉션이 어쩌고......

Primitives는 일반적으로 스택에 저장되고.......

힙 파편화는 메모리 할당, 해제가 반복되면서 어쩌고.... 디스크 조각모음 어쩌고....

 

 

 

힙 파편화가 아니라, 내 머리 속의 파편부터 GC하고 싶은 출근길이었다..

 

 

정리하자 정리

 

 

 

 

JavaScript에서의 메모리

 

 

 

 

 

기본적으로 JavaScript는 고수준의 언어이다. 변수, 함수, 객체 등을 만들때 JavaScript 엔진(V8 등)은 자동으로 메모리를 할당(Allocate)하고, 더 이상 필요로 하지 않을 때 가비지 컬렉션에 의해 않을때 자동으로 해제된다.  그래서 메모리의 생명주기를 얘기할 때 할당(allocate)한다 > 사용(use, references)한다 > 해제(release)의 흐름으로 표현한다.

 

메모리는 JavaScript 엔진의 힙(Memory Heap)과 스택(Call Stack)에 저장된다. 힙과 스택은 JavaScript이 서로 다른 목적으로 사용하는 데이터의 구조이다.

 

 

 

스택(Stack)

스택은 함수가 호출될 때마다 생성되는 실행 컨텍스트(Execution Context)와 원시 타입 값이 저장되는 영역이다. 여기에는 객체나 함수를 가리키는 참조 주소를 포함한다.

 

(이 포스팅에서는 렉시컬 환경에 대한 개념 설명을 포함하지 않는다.)

 

 

 

 

JavaScript는 동적 타입 언어이지만, 엔진은 원시 타입 값들의 크기와 수명에 대한 예측이 가능하다.

원시 타입은 구조가 단순하고, 내부적으로 고정된 포맷으로 저장된다. 예를 들어, number은 대부분 IEEE 754표준의 64비트 부동소수점 형식을 따르며, boolean은 보통 1바이트로 표현된다.

또한 원시 타입은 대부분 함수 내부에서 선언되며, 함수가 종료되면 해당 스택 프레임과 함께 자동으로 해제된다. 객체처럼 여러 참조가 동일한 메모리를 공유하지 않기 때문에, 값의 수명 역시 명확하게 예측 가능하다.

 

이러한 여러 값들은 LIFO인 콜 스택에 저장하여 빠르고 효율적인 접근이 가능하도록 설계되어 있다.

스택은 연속된 메모리 공간을 사용하므로 CPU 캐시 적중률이 높고, 값의 스코프가 명확하여 GC의 개입 없이도 메모리 해제가 자동으로 이루어진다. 단, 브라우저나 JS 엔진마다 스택의 최대 크기와 내부 구현은 다를 수 있다.

 

 

 

힙(Heap)

힙은 JavaScript의 객체, 함수를 저장하는 또다른 공간이다. 스택과 달리 예측이 불가능하기 때문에 동적 메모리를 할당한다. 런타임시에 동적 데이터들이 메모리를 할당받아서 저장되게된다.

 

 

원시 값이 아닌 경우 스택에서는 힙의 객체에 대한 참조(References)를 저장한다. 참조는 일종의 메모리 주소라고 생각하는 것이 편하게 접근이 가능하다. 다시 정리하면, 힙에서 새 객체가 생성되고 스택에는 참조가 생성된다.

 

 

V8 메모리구조: https://deepu.tech/memory-management-in-v8

 

 

JavaScript의 메모리 구조에서 가장 큰 부분을 차지하는 힙 메모리는, 여러 공간들로 나뉘어 관리되는데 V8에서는 New space, Old space로 나뉘어 관리된다.

  • New space: 새로 생성된 객체가 저장되는 영역이다. 객체가 자주 생성되고 삭제되는 애플리케이션의 특성상 빠르고 효율적인 가비지 컬렉션이 가능하도록 설계되어있다.
  • Old space: New space에서 가비지 컬렉션의 대상이 되지 않은 오래된 객체가 저장되는 영역이다. 이 영역의 객체들은 비교적 수명이 길고 New space보다 가비지 컬렉션이 덜 수행된다.

 

 

가비지 컬렉션과 메모리 해제

가비지 컬렉션은 New space와 Old space에서 다른 방식으로 동작한다. 위에서 잠깐 언급했듯이 각 영역 별로 최적화된 minor, major GC로 관리한다. New space는 객체 생명 주기가 짧아 빠르게 가비지 컬렉션을 수행하고, 수명이 길고 메모리 사이즈가 큰 Old space는 major GC가 가비지 컬렉션을 수행한다.

 

 

minor GC (Scavenger)

minor GC는 New space의 가비지 컬렉션을 담당하는 GC이다. New space는 두 개의 semi space로 관리되는데, 객체들이 머무르는 영역은 From space라 부르며 GC의 대상이 되지 않은 객체들이 잠시 머무르는 영역을 to space라 부른다.

 

 

to space는 GC가 동작하지 않을 때는 비어있는 상태로 시작한다. from space에 있는 객체들 중 살아남은 객체들만 to space로 복사하고, 그 후 from space는 초기화된다. 복사가 완료되면 to space는 새로운 from space로 전환되고, 초기화된 이전 from space는 다음 GC를 위한 to space가 된다. 이러한 단순한 복사 + 전체 초기화 매커니즘 덕에 minor GC는 최적화된 빠른 가비지 컬렉션을 수행할 수 있다.

JavaScript는 특성상 짧은 생명 주기를 가진 객체가 매우 많다. 콜백, 클로저, 이벤트 핸들러 등의 실행 흐름은 순간적으로 많은 객체를 만들고 금방 사라지게 만든다. 이런 특성 때문에 V8은 Scavenge 기반의 minor GC 전략을 통해 효율적으로 메모리를 관리할 수 있다.

단, 객체가 여러 번 minor GC를 거쳐 살아남게 되면, 이제 더 이상 short-lived하지 않다고 판단되어 Old Space로 승격된다.  이때부터는 Mark-and-Sweep 방식의 major GC가 동작하게 된다.

 

 

major GC

major GC는 오래 살아남은 객체들(Old space)에 대해 더이상 참조되지 않는 더이상 쓸모없는 객체로 간주한다.

Mark-Sweep-Compact, Tri-color 알고리즘을 사용해 도달할 수 없는 객체를 제거하거나 메모리를 압축한다.

 

 

마킹(Mark)

GC Roots라는 전역 객체, 실행 중인 함수, 클로저 등을 담고있는 곳에서 출발하여 도달 가능한 객체(Reachable)을 전부 dfs로 순회하면서 마킹을 한다. 루트에서 닿을 수 없는 객체는 마킹되지 않고 GC의 대상이 된다.

 

 

1. 루트 객체(Roots) 수집 및 마킹 시작

GC가 시작되면 deque 구조의 marking worklist tri-color 알고리즘을 활용하여 마킹을 수행한다.

초기에는 marking worklist가 비어있고, 모든 객체는 white 상태이다. 이후 Roots는 바로 grey 상태가 되어 marking worklist에 삽입(push front) 다.

  • white(00): 아직 탐색되지 않은 객체
  • grey(10): 발견됐지만 아직 내부 탐색되지 않음 (작업 대기중)
  • black(11): 완전히 마킹 완료된 객체 (내부 필드까지 탐색 완료)

 

Roots를 gray로 marking

 

 

 

2. worklist에서 꺼낸(pop front) 객체를 black으로 마킹한다.

 

꺼낸 객체가 참조하는 모든 필드를 따라가면서 새롭게 참조되는 white 객체들을 gray로 마킹하며 worklist에 추가한다. 이 때, white인 객체만 push front하며 이미 방문된 객체들은 worklist에 추가하지 않는다.

 

 

 

gray를 꺼내어 marking하는 과정

 

 

3. 모두 black이 되거나 white가 될 때 까지 위 과정을 반복한다.

 

마킹되지 않은 대상인 white 객체들은 GC의 대상으로 정리(sweep)된다.

 

dfs를 통한 순회

 

 

마킹 과정은, 객체 간의 참조 그래프를 DFS를 통해 순회하여 이루어진다. 이 때, 중요한 점은 탐색 중인 객체 그래프가 외부에 의해 변경되지 않아야 한다는 점이다. 만약 마킹 도중 애플리케이션이 동작하면서 객체 간 참조가 추가되거나 삭제된다면, GC는 이미 탐색을 마친 객체를 놓치거나 이미 삭제된 참조를 따라가며 잘못된 객체를 가지고 있는 등의 문제가 발생할 수 있다. 이는 곧 살아있는 객체를 실수로 마킹하지 않거나 필요한 객체를 마킹하는 등 심각한 오류로 이어질 수 있다.

 

GC가 객체 그래프 전체를 안전하게 순회할 수 있도록 보장하기 위해서 마킹 단계에서 애플리케이션을 일시 중단(stop-the-world)시킨다. 이를 통해 객체 간 관계를 고정시켜 DFS를 안정적으로 수행할 수 있다.

 

GC가 동작할 때 애플리케이션은 잠시 멈춘다.

 

 

다만, stop-the-world는 앱의 응답성에 직접적인 영향을 주기 때문에, V8은 Parallel, Incremental Marking, Concurrent Marking과 같은 기술을 도입해 정지 시간을 최소화하면서도 객체 그래프의 정합성을 유지할 수 있는 방식을 사용하고 있다.

  • Parallel: 메인 스레드와 헬퍼 스레드가 거의 같은 양의 작업을 동시에 수행하는 방식이다. 총 일시 정지 시간은 스레드 수에 반비례하여 애플리케이션 중지 시간이 대폭 단축된다.
  • Incremental: 메인 스레드가 적은 양의 작업을 간헐적으로 수행한다. 메인 스레드에서 GC에 소요되는 시간을 줄이지 않고 분산시키는 방식으로 메인 스레드의 stop-the-world 시간을 줄일 수 있다.
  • Concurrent: 메인 스레드는 GC 작업을 수행하지 않고, 헬퍼 스레드가 백그라운드에서 GC를 100% 수행한다. JavaScript의 힙이 언제든지 변경될 수 있고, 동시성 문제가 있으므로 읽기/쓰기 경쟁에서 자유롭지 못하다. 메인 스레드의 stop-the-world는 0에 수렴하지만, 헬퍼 스레드와의 동시성 동기화 문제 때문에 약간의 오버헤드가 있다.

Parallel

 

Incremental Marking

 

Concurrent Marking

 

 

 

스위핑(Sweeping)

white으로 마킹된 객체는 도달 가능하지 못한 대상(Unreachable)으로 판단하여 정리 대상이 된다. 이 객체들의 메모리 주소를 free list에 추가한다. 여기서 free list란 linked list 구조로 동적 메모리 할당을 위해 사용되는 자료구조이다.

 

V8은 힙에서 무조건 새로운 메모리를 요청하지 않고, free list에 있는 빈 슬롯을 먼저 조회해서 해당 메모리 주소를 재사용하게 된다.

 

 

압축(Compact)

GC가 끝난 후 힙 메모리 내부에 여기저기 흩어지게 되는데, 이를 메모리 단편화라고 한다.

메모리 단편화가 심할 경우에만 조건적으로 compact를 수행한다.

 

 

 

 

마무리

포스팅을 정리하면서 파편화된 지식을 압축(Compact)하여 내가 면접 질문에서 서론과 같은 질문을 받았을 때에 대처할 수 있도록 압축한 내용을 다시 메모리(?)에 올려놓고 마무리한다.

 

  • 스택은 구조상 크기와 수명이 명확한 값들을 저장하기에 적합하며, 특히 선형적인 구조로 인해 관리가 빠르다. 하지만 무한정 커질 수 있다고 해도 스택의 선형적인 구조가 객체 참조와 같은 동적 패턴을 효율적으로 처리할 수 있을지는 의문이다.
  • 힙은 런타임 중에 크기가 가변적이며, 현재로서는 참조 기반 객체 데이터가 저장되는 유일한 구조이다. 만약 참조 기반 데이터를 스택에 담는다면, 스택에 담을 수는 있겠지만 객체 간 참조를 효율적으로 관리할 수 없고 결국 연속적인 메모리 탐색이 필요하며 최악에는 모든 메모리에 대한 탐색이 필요할 수도 있다. 사과 하나를 꺼내기 위해 냉장고에 있는 모든 음식들을 꺼내는 것 처럼 말이다.

즉, 아무리 스택의 스펙이 좋아진다고 하더라도 힙의 역할을 구조적으로 대체할 수 없다고 생각한다.

 

현재 내 백엔드 스택과 연관되어 JavaScript와 연계된 꼬리질문이 이어진다면

  • V8은 GC 최적화(Minor/Major GC와 구현된 알고리즘)를 통해 힙 메모리의 파편화 문제를 실질적으로 해결하고 있다.
  • 물론 스택 메모리 자체도 V8 내부 최적화(Inlining, Espace Analysis)를 통해 더 빠르게 동작하도록 설계되어 있다. 다만 동적 객체의 수명 주기를 다루는 영역에서 힙은 여전히 핵심적인 역할이다.

 

 

 

 

 

참조

https://v8.dev/blog/concurrent-marking

https://v8.dev/blog/trash-talk

https://felixgerschau.com/javascript-memory-management/

https://fe-developers.kakaoent.com/2022/220519-garbage-collection/

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[JavaScript] 식사 메뉴를 선정을 위한 돌림판(룰렛) 만들기

Tech/JS & TS 2023. 3. 15. 07:32
728x90
728x90

결정장애라 메뉴선정시에 항상 어려움을 겪곤 한다.

학원 수강 중 메뉴 선정에 대한 고민을 하였고, 결국 오픈소스를 활용하여 돌림판 제작을 시작했다.

 

 

See the Pen Untitled by magic (@mag11c) on CodePen.

 

 

자주가는 식당들을 product 배열에 미리 추가한 뒤,

해당 배열의 길이와 동일하게 색상 배열을 만들어 색상을 미리 추가시켰다.

그리고 새로운 메뉴를 추가할 수 있다.

const $c = document.querySelector("canvas");
const ctx = $c.getContext(`2d`);
const menuAdd = document.querySelector('#menuAdd');
const product = ["햄버거", "순대국", "정식당", "중국집", "구내식당"];
const colors = [];

const newMake = () => {
	const [cw, ch] = [$c.width / 2, $c.height / 2];
	const arc = Math.PI / (product.length / 2);  
	for (let i = 0; i < product.length; i++) {
		ctx.beginPath();
		if(colors.length == 0){
			for(var l=0; l<product.length; l++){
				let r = Math.floor(Math.random() * 256);
				let g = Math.floor(Math.random() * 256);
				let b = Math.floor(Math.random() * 256);
				colors.push("rgb(" + r + "," + g + "," + b + ")");
			}
		}
		ctx.fillStyle = colors[i % (colors.length)];
		ctx.moveTo(cw, ch);
		ctx.arc(cw, ch, cw, arc * (i - 1), arc * i);
		ctx.fill();
		ctx.closePath();
	}

	ctx.fillStyle = "#fff";
	ctx.font = "18px Pretendard";
	ctx.textAlign = "center";

	for (let i = 0; i < product.length; i++) {
		const angle = (arc * i) + (arc / 2);

		ctx.save();

		ctx.translate(
			cw + Math.cos(angle) * (cw - 50),
			ch + Math.sin(angle) * (ch - 50)
		);

		ctx.rotate(angle + Math.PI / 2);

		product[i].split(" ").forEach((text, j) => {
			ctx.fillText(text, 0, 30 * j);
		});

		ctx.restore();
	}
}

const rotate = () => {
	$c.style.transform = `initial`;
	$c.style.transition = `initial`;
	const alpha = Math.floor(Math.random()*100);

	setTimeout(() => {    
		const ran = Math.floor(Math.random() * product.length);
		const arc = 360 / product.length;
		const rotate = (ran * arc) + 3600 + (arc * 3) - (arc/4) + alpha;
		$c.style.transform = `rotate(-${rotate}deg)`;
		$c.style.transition = `2s`;
    
	}, 1);
};


function add(){
	if(menuAdd.value != undefined && menuAdd.value != ""){
		product.push(menuAdd.value);
		let r = Math.floor(Math.random() * 256);
		let g = Math.floor(Math.random() * 256);
		let b = Math.floor(Math.random() * 256);
		colors.push("rgb(" + r + "," + g + "," + b + ")");
		newMake();
		menuAdd.value="";
	}
	else{
		alert("메뉴를 입력한 후 버튼을 클릭 해 주세요");
	}
}

newMake();

 

 

 

참조


아래 블로그의 오픈소스를 활용하여 제 상황에 맞게 약간 변경하였습니다.

https://gurtn.tistory.com/180

 

[JS] 룰렛 구현하기

JavaScript와 캔버스(Canvas)를 사용하여 룰렛을 구현해보았습니다. 코드 See the Pen Canvas Roulette by hyukson (@hyukson) on CodePen. 코드 풀이 const $c = document.querySelector("canvas"); const ctx = $c.getContext(`2d`); // 룰렛

gurtn.tistory.com

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[JavaScript] localStorage를 활용한 todoList 만들기

Tech/JS & TS 2023. 1. 22. 21:05
728x90
728x90

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="style.css" />
    <title>해야할이일</title>
  </head>
  <body>
    <h1>todos</h1>
    <form id="form">
      <input type="text" class="input" id="input" placeholder="Enter your todo" autocomplete="off">

      <ul class="todos" id="todos">

      </ul>
    </form>
    <small>Left click to toggle completed. <br> Right click to delete todo</small>

    <script src="script.js"></script>
  </body>
</html>

 

input에서 입력된 값을 ul의 li에다가 담을 것이며, 스토리지를 이용해 새로고침해도 리스트가 남아있게 할 예정이다.

자바스크립트 공부를 위함이니 css는 맨 아래에다가 작성 해 두겠다.

 

JS

새로고침 이후에도 유지되기 위해 스토리지를 활용할 것이다.

localStorage에 데이터를 넘겨주기 위해 배열을 JSON.stringify() 메서드로 포맷팅한 뒤 넘겨주었다.

const form = document.querySelector('form');
const input = document.querySelector('input');
const ul = document.querySelector('ul');

const todos = "TODOS";
let arr = new Array();

// localStorage.clear();

//저장 시 obj -> string
function save(){
    localStorage.setItem(todos, JSON.stringify(arr))
}

function put(text){
    //필요한 li 생성작업
    const li = document.createElement('li');
    const liId = arr.length + 1;
    li.textContent = text;
    ul.appendChild(li);
    li.id = liId;
    
    //json형태로 정보저장
    const Obj = {
        id : liId,
        text,
    };

    arr.push(Obj);
    save();
}


function submit(e){
    //form기능 중단
    e.preventDefault();
    const curVal = input.value;
    put(curVal);
    //form기능을 멈췄기때문에 put뒤에 반드시 벨류초기화
    input.value='';
}

function load(){
    const loading = localStorage.getItem(todos);

    //스토리지에 값이 있나 확인하는 작업.
    if(loading!==null){
        const json = JSON.parse(loading); //stringify -> json(Obj)
        //jsonobj중 text만 받아서 put(li에 추가)   
        json.forEach(function (j){
            put(j.text);
        })
    }
}

//init에서 load를 불러오고, submit기능중단을 위한 form의 이벤트 추가
function init(){
    load();
    form.addEventListener('submit', submit);
}

init();

 

CSS

@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@200;400&display=swap');

* {
  box-sizing: border-box;
}

body {
  background-color: #f5f5f5;
  color: #444;
  font-family: 'Poppins', sans-serif;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  margin: 0;
}

h1 {
  color: rgb(179, 131, 226);
  font-size: 10rem;
  text-align: center;
  opacity: 0.4;
}

form {
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
  max-width: 100%;
  width: 400px;
}

.input {
  border: none;
  color: #444;
  font-size: 2rem;
  padding: 1rem 2rem;
  display: block;
  width: 100%;
}

.input::placeholder {
  color: #d5d5d5;
}

.input:focus {
  outline-color: rgb(179, 131, 226);
}

.todos {
  background-color: #fff;
  padding: 0;
  margin: 0;
  list-style-type: none;
}

.todos li {
  border-top: 1px solid #e5e5e5;
  cursor: pointer;
  font-size: 1.5rem;
  padding: 1rem 2rem;
}

.todos li.completed {
  color: #b6b6b6;
  text-decoration: line-through;
}

small {
  color: #b5b5b5;
  margin-top: 3rem;
  text-align: center;
}
728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[JavaScript] 웹 스토리지(Web Storage)

Tech/JS & TS 2023. 1. 22. 20:14
728x90
728x90

웹 스토리지 ( Web Storage )


기존 쿠키의 문제점을 보완하기 위해 사용한다.

https://mag1c.tistory.com/187
 

쿠키와 세션 (Cookie & Session)

HTTP 프로토콜의 특징 비연결성 ( Connectionless ) 클라이언트가 서버에 요청(Request)할 때, 그에 대한 응답(Response)을 한 후, 연결을 끊는다. 비상태성 ( Stateless ) 클라이언트의 상태 정보를 가지지 않는

mag1c.tistory.com

 

 

웹 스토리지의 특징

1. 브라우저 내부에 Key, Value 쌍을 저장하는 공간이다.

2. 네트워크 요청 시 서버로 전송되지 쿠키에 비해 않아 많은 자료보관이 가능하다. (2MB이상)

3. 서버가 HTTP 헤더를 통해 스토리지 객체를 조작할 수 없다. 자바스크립트 내에서 조작이 가능하다.

4. 웹 스토리지 객체는 오리진(origin)에 묶여있다. 따라서 프로토콜과 서브 도메인이 다르면 데이터에 접근할 수 없다.

오리진(origin)영역

도메인, 프로토콜, 포트로 정의되는 영역

 

//사용자의 브라우저가 웹 스토리지의 지원 여부를 확인한다.
if(typeof(Storage)!=="undefined") alert("사용가능")
else alert("사용 불가")

 

 

스토리지 또한 개발자모드(F12)의 Application 탭에서 확인 가능하다.

 

 

메서드

메서드 설명
setItem(key, value) key, value 저장
getItem(key) key에 해당하는 value 받기
removeItem(key) 해당 key와 그에 맞는 value 삭제
clear() 스토리지 초기화
key(index) 인덱스에 해당하는 key값 받기
length 저장된 개수

 

 

localStorage vs sessionStorage

브라우저를 껐다가 켜도 데이터가 남아있다 - localStorage

페이지를 새로고침해도 데이터가 남아있지만, 브라우저를 종료하면 사라진다 - sessionStorage

 

 

 

 

사용예제


<div id="cnt"></div>
<p><button id="btn">버튼</button></p>

<script>
    const btn = document.querySelector('#btn');
    btn.addEventListener('click', function(){
        if(typeof(Storage) !=="undefined"){
            if(localStorage.cnt){
                localStorage.cnt = Number(localStorage.cnt) +1;
            }else{
                localStorage.cnt = 1;
            }
            document.querySelector("#cnt").innerHTML="세션카운트 : " + localStorage.cnt;
        }
    })
</script>

 

 

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[JavaScript] 쿠키(Cookie) 사용하기

Tech/JS & TS 2023. 1. 22. 16:43
728x90
728x90

쿠키에 대한 자세한 설명은 아래에

https://mag1c.tistory.com/187
 

쿠키와 세션 (Cookie & Session)

HTTP 프로토콜의 특징 비연결성 ( Connectionless ) 클라이언트가 서버에 요청(Request)할 때, 그에 대한 응답(Response)을 한 후, 연결을 끊는다. 비상태성 ( Stateless ) 클라이언트의 상태 정보를 가지지 않는

mag1c.tistory.com


 

 

 

자바스크립트에서의 Cookie 사용하기


document.cookie 프로퍼티를 이용하여 생성, 삭제 및 조회가 가능하다.

 

document.cookie는 name=value 쌍으로 구성되어 있고, 각 쌍은  ; 로 구분한다.

쌍 하나는 하나의 독립된 쿠키를 나타내며,  ; 을 기준으로 document.cookie의 값을 분리하면 원하는 쿠키를 찾을 수 있다.

document.cookie에 값을 할당하면, 브라우저는 이 값을 받아 해당 쿠키를 갱신하며, 다른 쿠키의 값은 변경되지 않는다.

//Name이 'user'인 쿠키의 Value만 갱신
document.cookie = "user=John";

쿠키의 이름과 값엔 제약이 없어 모든 글자가 허용된다.

형식의 유효성을 일관성있게 유지하기 위해 내장함수 encodeURIComponent를 사용하여 이스케이프 처리를 해야 한다.

let name = "name";
let value = "John Smith";

//인코딩 처리 -> my%20name=John%20Smith
document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value);
쿠키의 한계
1. encodeURIComponent로 인코딩한 후의 name=value 쌍이 4KB를 넘으면 쿠키에 저장할 수 없다.
2. 도메인 하나당 저장할 수 있는 쿠키의 수는 20여개로 한정되어 있다. 개수는 브라우저에 따라 차이가 있다.

 

 

 

쿠키의 옵션


쿠키에는 여러 옵션들이 존재한다.

 

 

개발자도구(F12) Application >> Cookies에서 쿠키를 확인할 수 있다.

 

1. Name & Value

데이터를 저장하고 읽는 데 사용하는 옵션으로 반드시 지정해주어야 한다.

document.cookie = "user=John"

 

2. Path

옵션을 입력하지 않으면 현재 도메인의 경로로 자동 입력된다.

특별한 경우가 아니라면 path=/와 같이 루트로 설정해 웹사이트의 모든 페이지에서 쿠키에 접근할 수 있도록 해야한다.

예를들어, path=/admin으로 설정한 쿠키는 /admin과 그 하위 경로에는 접근 가능하지만
/home과 같은 다른 경로에서는 사용할 수 없다.
document.cookie = "user=John; path=/"

 

3. Domain

쿠키에 접근 가능한 domain을 지정한다.

옵션을 입력하지 않으면 현재 domain의 경로로 자동 입력된다.

domain에 루트도메인을 명시적으로 설정하여 서브 도메인에서도 메인 도메인에서 생성한 쿠키 정보를 사용할 수 있다.

document.cookie = "user=John; domain=site.com"

 

4. Expires / Max-age

expires(유효 일자), max-age(만료 일자) 옵션을 지정하여, 쿠키의 만료 기간을 설정할 수 있다.

expires는 GMT 혹은 UTC포맷으로 설정해야한다.

세션 쿠키(Session Cookie)
expires, max-age 옵션이 지정되어있지 않아, 브라우저가 닫힐 때 삭제되는 쿠키.

max-age는 expires의 대안으로, 쿠키 만료기간을 설정할 수 있게 해준다.

현재부터 설정하고자 하는 만료일시까지의 시간을 로 환산한 값을 설정한다.

//expires
//toUTCString() 메서드를 사용하여 UTC포맷으로 쉽게 변환이 가능하다.
let date = new Date(Date.now() + 86400e3);
date = date.toUTCString();
document.cookie = "user=John; expires=" + date;

//max-age
document.cookie = "user=John; max-age=3600";

 

5. Secure

해당 옵션을 설정 시, HTTPS로 통신하는 경우에만 쿠키가 전송된다.

// 설정한 쿠키는 HTTPS 통신시에만 접근할 수 있음
document.cookie = "user=John; secure";

 

6. HttpOnly

HttpOnly 옵션이 설정된 쿠키는 document.cookie로 쿠키 정보를 읽을 수 없다.

document.cookie = "user=John; httpOnly"

 

7. Samesite

XSRF 공격을 막기 위해 만들어진 옵션이다.

samesite 옵션을 XSRF 토큰 같은 다른 보안기법과 함께 사용하면 보안을 강화할 수 있다.

2017년 이전 버전의 브라우저에서는 samesite를 지원하지 않는다.

samesite(=strict)
사이트 외부에서 요청을 보낼 때, 해당 옵션이 있는 쿠키는 절대로 전송되지 않는다.

samesite=lax
GET방식의 작업 혹은 최상위 레벨 탐색(브라우저 주소창에서 URL변경 등)에서의 작업이 이루어질 때만 쿠키가 서버로 전송된다.
document.cookie = "user=John; samesite"

 

 

 

쿠키 함수


getCookie(name)

정규 표현식을 사용하여 가장 빠르게 접근할 수 있다.

// 주어진 이름의 쿠키를 반환하는데,
// 조건에 맞는 쿠키가 없다면 undefined를 반환합니다.
function getCookie(name) {
  let matches = document.cookie.match(new RegExp(
    "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
  ));
  return matches ? decodeURIComponent(matches[1]) : undefined;
}

 

setCookie(name, value, options)

function setCookie(name, value, options = {}) {

  options = {
    path: '/',
    // 필요한 경우, 옵션 기본값을 설정할 수도 있습니다.
    ...options
  };

  if (options.expires instanceof Date) {
    options.expires = options.expires.toUTCString();
  }

  let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value);

  for (let optionKey in options) {
    updatedCookie += "; " + optionKey;
    let optionValue = options[optionKey];
    if (optionValue !== true) {
      updatedCookie += "=" + optionValue;
    }
  }

  document.cookie = updatedCookie;
}

// Example of use:
setCookie('user', 'John', {secure: true, 'max-age': 3600});

 

deleteCookie(name)

//만료 기간을 음수로 설정하여 쿠키를 삭제
function deleteCookie(name) {
  setCookie(name, "", {
    'max-age': -1
  })
}

 

 

더보기
728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[JavaScript] async await

Tech/JS & TS 2023. 1. 21. 15:19
728x90
728x90

Promise사용 시 불편한 점들


1. then()을 연속적으로 호출하여 문제 발생 시 혼란을 겪을 수 있다.

2. 복잡한 구조의 비동기처리 코드 작성 시 코드의 가독성이 저하된다.

3. 동기코드와 비동기코드가 섞여있을 때 예외처리 하기가 난해하다.

 

 

 

async / await


Promise의 불편한 점들을 해결하기 위해 ES7에서 async/await가 추가되었다.

async function f() {
  return 1;
}

f().then(alert); // 1

async는 function 앞에 위치하며, async가 붙은 함수는 항상 Promise를 반환한다.

Promise객체를 생성하지 않더라도 Promise 객체가 리턴되는 것에 주의하자.

await는 async함수 내부에서만 동작한다.

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("완료!"), 1000)
  });

  let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)

  alert(result); // "완료!"
}

f();

await는 Promise가 처리될 때 까지 함수 실행을 기다리게 만드는 특징이 있다.

비동기 작업의 결과값을 얻을 때 까지 대기했다가 그 결과값과 함께 실행이 재개된다.

Promise가 처리되길 기다리는 동안에 다른 일(다른 스크립트 실행 및 이벤트 처리 등)을 할 수 있기 때문에 효율적이다.

 

프라미스 체이닝을 async/await로 변경하기

아래의 프라미스체이닝 예시를 async/await를 사용해 다시 작성해보자.

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise(function(resolve, reject) { // (*)
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser); // (**)
    }, 3000);
  }))
  .then(githubUser => alert(`${githubUser.name}의 이미지를 성공적으로 출력하였습니다.`));

then 호출을 await로 변경하고, function앞에 async를 붙여 await를 사용할 수 있게 하면 된다.

async function showAvatar() {

  // JSON 읽기
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();

  // github 사용자 정보 읽기
  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
  let githubUser = await githubResponse.json();

  // 아바타 보여주기
  let img = document.createElement('img');
  img.src = githubUser.avatar_url;
  img.className = "promise-avatar-example";
  document.body.append(img);

  // 3초 대기
  await new Promise((resolve, reject) => setTimeout(resolve, 3000));

  img.remove();

  return githubUser;
}

showAvatar();

 

 

 

예외처리


그저 try/catch로 일관되게 예외처리를 할 수 있다.

function getTitle(){
  const response = fetch("https://jsonplaceholder.typicode.com/posts");
  return response.then(res => res.json());
}

async function exec(){
  var text;
  try {
    text = await getTitle();
    console.log(text[0].title);
  }
  catch(error){
    console.log(error);
  }
}
exec();

 

 

 

더보기
728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[JavaScript] Promise

Tech/JS & TS 2023. 1. 20. 09:23
728x90
728x90

프로미스(Promise)


비동기 처리를 위해 사용되는 객체이며, 콜백 패턴의 한계를 보완하기 위해 사용한다.

 

장점

  • 비동기 처리 시점을 명확하게 표현 가능
  • 연속된 비동기 처리 작업을 수행하기 편하며, 작업 상태를 쉽게 확인 가능
  • 코드의 유지보수성이 증가.

 

const promise = () => new Promise((resolve, reject) => {
    let sum = 1 + 1
    if(sum == 2) {
        resolve('correct')
    } else {
        reject('incorrect')
    }
})

promise().then((message) => {
    console.log(message)
}).catch((message) => {
    console.log(message)
})
resolve와 reject
resolve와 reject는 자바스크립트에서 자체 제공하는 콜백이다.

resolve(value) : 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
reject(error) : 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
후속 처리 메서드
then
- 두 개의 콜백 함수를 파라미터로 전달받으며, 기본적으로 프로미스를 반환한다.
- 첫 번째 콜백 함수는 성공 시 실행된다.
- 두 번째 콜백 함수는 실패 시 실행된다.

catch
- 비동기 처리 혹은 then 메서드 실행 중 발생한 에러(예외)가 발생하면 호출되며, 프로미스를 반환한다.

 

 

 

프로미스의 상태


프로미스는 생성하고 종료될 때 까지 3가지의 상태를 가진다.

위에 작성했던 코드로 프로미스의 3가지 상태를 알아보자.

1. Pending(대기)

비동기 처리 로직이 완료되지 않은 상태

//생성 시 대기상태가 된다.
const promise = () => new Promise((resolve, reject)

2. Fulfiled(이행)

resolve를 실행하면, 이행 상태가 되며, 이행 시 then을 통해 결과값을 받을 수 있다.

const promise = () => new Promise((resolve, reject) => {
    let sum = 1 + 1
    if(sum == 2) {
        resolve('correct')
    } else {
        reject('incorrect')
    }
})

promise().then((message) => {
    console.log(message)
}).catch((message) => {
    console.log(message)
})

3. Rejected(실패)

Fulfiled의 코드에서, 실패 시 reject를 호출하고, 실패 시의 값을 catch로 받을 수 있다.

 

출처 :&nbsp;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise

 

 

 

에러 처리방법


//then의 두 번째 파라미터로 error를 받아서 처리.
promise().then(message, error);

//catch를 이용한 처리를 더 권장한다.
promise().then((message) => {
    console.log(message)
}).catch((message) => {
    console.log(message)
})

 

 

 

프로미스의 연결 ( Promise Chaining )


여러개의 프로미스를 연결하여 사용 가능하다.

then()메서드를 호출하여 새로운 프로미스 객체가 반환되고, 이에 따른 후속처리를 계속 진행할 수 있다.

const url = 'https://jsonplaceholder.typicode.com/posts';    

    // 포스트 id가 1인 포스트를 검색하고 프로미스를 반환한다.
    promiseAjax('GET', `${url}/1`)
      // 포스트 id가 1인 포스트를 작성한 사용자의 아이디로 작성된 모든 포스트를 검색하고 프로미스를 반환한다.
      .then(res => promiseAjax('GET', `${url}?userId=${JSON.parse(res).userId}`))
      .then(JSON.parse)
      .then(render)
      .catch(console.error);

 

 

 

프로미스 사용 예시


프로미스를 이용하여 커지는 원을 만들어보자.

See the Pen Untitled by magic (@mag11c) on CodePen.

 

 

더보기

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

[JavaScript] 콜백 함수

Tech/JS & TS 2023. 1. 19. 23:30
728x90
728x90

콜백 함수 ( Callback Function )


함수 안에 실행하는 또 다른 함수이며 파라미터로 함수를 전달하는 함수이다.

또한 함수 이름없이 익명으로 전달이 가능한 함수를 말한다.

function introduce (lastName, firstName, callback) {
    var fullName = lastName + firstName;
    
    callback(fullName);
}
 
function hello (name) {
    console.log("제 이름은 " + name + "입니다");
}
 
function bye (name) {
    console.log("지금까지 " + name + "이었습니다.");
}
 
introduce("나", "지만", hello);
//제 이름은 나지만입니다
 
introduce("나", "지만", bye);
//지금까지 나지만이었습니다.

 

다른 동작을 수행하는 함수를 정의해두고 파라미터로 사용하면, 다른 동작들을 수행하는 것이 가능하다.

함수를 나눠줌으로써 코드의 재사용성이 극대화되며, 관리에 용이하다.

 

 

 

사용 원칙


1. 익명함수 사용

함수의 내부에서 실행되기 때문에 익명으로 사용이 가능하다.

let goat =["돌격", "막걸리", "아티스트"]
 
goat.forEach((i,idx)=>{
    console.log(idx + ":" + i);
});

 

2. 함수의 이름만 넘기기

함수를 콜백함수로 사용할 경우, 함수의 이름만 넘겨주면 된다.(함수를 인자로 사용할 때 ()를 붙일 필요가 없다.)

function whosYourDaddy(name, callback){
    console.log("우리 아부지 성함은 " + name +"입니다");
    callback();
}

function test(){
    console.log("니 내 아나?");
}

whosYourDaddy('김철수', test);

 

 

 

콜백 함수 주의사항 - this를 사용 시


콜백함수에서의 this는 기본적으로 값에 의한 호출(call by value)을 하기 때문에, window객체를 가리킨다.

const userData = {
    signUp: '2023-01-19 23:23:23',
    id: 'mag123c',
    name: 'what',
    setName: function(firstName, lastName) {
        this.name = firstName + ' ' + lastName;
    }
}

function getUserName(firstName, lastName, callback) {
    callback(firstName, lastName);
}
 
getUserName('Kim', 'ChulSoo', userData.setName);
 
console.log(userData.name); // Not Set
console.log(window.name); // Kim ChulSoo

call과 apply를 사용하여 this를 보호하자.

(...생략...)
function getUserName(firstName, lastName, callback) {
    callback.call(obj, firstName, lastName);
}
 
getUserName('Kim', 'ChulSoo', userData.setName, userData);
 
console.log(userData.name); // Kim ChulSoo
(...생략...)
function getUserName(firstName, lastName, callback) {
    callback.apply(obj, [firstName, lastName]);  //apply는 파라미터를 배열로 전달한다.
}
 
getUserName('Kim', 'ChulSoo', userData.setName, userData);
 
console.log(userData.name); // Kim ChulSoo

 

 

 

콜백 Hell


콜백 지옥 : 매개변수로 넘겨지는 콜백 함수가 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 수준으로 깊어지는 현상

비동기 방식의 장점은 다른 요청이 중단되지 않는다는 점이다.

하지만 비동기 호출이 자주 일어나는 경우 콜백 지옥이 발생한다.

step1(function(value1){
    step2(value1, function(value2){
        step3(value2, function(value3){
            step4(value3. function(value4){
                step5(value4, function(value5){
                    step6(value5, function(value6){
                        step7(value6, function(value7){
                            step8(value7, function(value8){
                                step9(value8, function(value9){
                                    step10(value9, function(value10));
                                });
                            });
                        });
                    });
                });
            });
        });
    });
});

 

 

 

 

더보기

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록