밀집 배열(dense array)

밀집 배열은 동일한 크기의 메모리 공간이 빈틈없이 연속적으로 나열된 자료구조이다.
배열의 요소는 하나의 데이터 타입으로 통일되어 있으며 서로 연속적으로 인접해 있다.
이러한 밀집 배열이 자료구조(data structure)에서 말하는 배열이다.

이러한 일반적인 의미의 배열은 각 요소가 동일한 데이터 크기를 갖고, 빈틈없이 연속적으로 이어져 있다.
그러기에 아래의 연산을 통해 단 한 번의 연산으로 임의의 요소에 접근할 수 있다.
검색 대상 요소의 메모리 주소 = 배열의 시작 메모리 주소 + 인덱스 * 요소의 바이트 수
이처럼 매유 효율적이고 고속으로 동작하는 방식을 임의 접근(random access)라고 부르며 시간 복잡도는 O(1)이다.

다만, 정렬이 되지 않은 배열에서 특정한 요소를 검색하는 경우는 선형 검색(inear search)를 통해 접근해야 한다.
또한, 배열의 요소를 삽입하거나 삭제하는 경우 배열의 요소를 연속적으로 유지하기 위해 요소를 이동시켜야 하는 단점도 있다.

참고) 선형 검색

모든 요소를 처음부터 특정 요소를 발견할 때 까지 차례대로 검색하는 방식이다. 시간 복잡도는 O(n)이다.

// 선형 검색을 통해 배열(array)에 특정 요소(target)가 존재하는지 확인한다.
// 배열에 특정 요소가 존재하면 특정 요소의 인덱스를 반환하고, 존재하지 않으면 -1을 반환한다.
function linearSearch(array, target) {
  const length = array.length;

  for (let i = 0; i < length; i++) {
    if (array[i] === target) return i;
  }

  return -1;
}

console.log(linearSearch([1, 2, 3, 4], 2)); // 1
console.log(linearSearch([1, 2, 3, 4], 5)); // -1

희소 배열(sparse array)

희소 배열은 앞에서 말한 일반적인 의미의 배열(밀집 배열)과 다르다.
배열의 요소를 위한 각각의 메모리 공간은 동일한 크기를 갖지 않아도 되며, 연속적으로 어어져 있지 않을 수도 있다.
자바스크립트의 배열은 희소 배열이다.
즉, 자바스크립트의 배열은 엄밀히 말해 일반적인 의미의 배열(밀집 배열)이 아닌 일반적인 배열의 동작을 흉내 낸 특수한 객체(희소 배열)인 것이다.

그러기 때문에 자바스크립트 배열의 타입은 Object인 것이다.

typeof [1, 2, 3]; // 'object'

자바스크립트의 배열

자바스크립트의 배열은 Object의 모든 특징을 포함하고 있으며, 배열만의 추가적인 특징들이 존재한다.

  • 인덱스 기반
  • 값의 순서
  • length 프로퍼티
    인덱스 기반의 배열은 인덱스와 요소로 이루어져 있으며 값에 대한 참조를 인덱스를 통해 한다.
    객체와는 다르게 값에 순서가 있다.
    length 프로퍼티(속성)을 지닌다는 것도 배열만의 추가적인 특징이다.

일반적인 배열과 자바스크립트 배열의 장단점

일반적인 배열은 인덱스로 요소에 빠르게 접근할 수 있다. 하지만 요소를 삽입 또는 삭제하는 경우에는 효율적이지 않다.
반면 자바스크립트 배열은 인덱스로 배열 요소에 접근하는 경우는 일반 배열보다 느리지만, 요소를 삭제, 삽입하는 경우에는 일반 배열보다 빠르다.

또한, 인덱스로 배열 요소에 접근할 때 일반적인 배열보다 느릴 수밖에 없는 구조적인 단점을 보완하기 위해 대부분의 모던 자바스크립트 엔진은 배열을 일반 객체와 구별하여 좀 더 배열처럼 동작하도록 최적화해 구현했다고 한다.
아래의 코드를 통해 확인할 수 있다.

const arr = [];

console.time("Array");

for (let i = 0; i < 1000000; i++) {
  arr[i] = i;
}
console.timeEnd("Array");
// 14ms


const obj = {};

console.time("Object");

for (let i = 0; i < 1000000; i++) {
  obj[i] = i;
}
console.timeEnd("Object");
// 26ms

'JavaScript > JavaScript' 카테고리의 다른 글

옵셔널 체이닝(Optional Chaining)  (1) 2024.02.25
JavaScript에서 접근자 프로퍼티와 캡슐화  (0) 2023.10.11
JS 문자열 비교 연산자  (1) 2023.10.06
호이스팅(Hoisting)  (0) 2023.09.27
sort 메소드  (0) 2023.09.06

객체의 프로퍼티(속성)는 점표기법을 통해 접근한다.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};

const catName = adventurer.cat.name;
console.log(catName); // Dinah

다만, 이렇게 중첩된 객체를 다룰 때 조심해야 할 부분이 있다.

const adventurer = {
  name: 'Jake',
};

const catName = adventurer.cat.name;
console.log(catName);

이렇게 cat 프로퍼티를 가지고 있지 않은 adventurer은 cat 프로퍼티가 undefined이므로 adventurer.cat.name에 접근하면 에러가 발생한다.

그래서 catName과 같이 중첩된 객체의 프로퍼티를 다룰 때는 adventurer.cat.name에 접근하기 전에 adventurer.cat이 null 또는 undefined가 아님을 확인하고 접근해야 에러를 방지할 수 있다.

이를 해결하기 위해서는 두개의 방식이 사용된다.

  1. if문 또는 AND 연산자
    // if 문
    if (adventurer.cat) {
    console.log(adventurer.cat.name);
    }
    

// AND 연산자
console.log(adventurer.cat && adventurer.cat.name)

AND 연산자를 이렇게 활용할 수 있는 이유는 `1-12section.js`에 있는 SCE 때문이다.

2. Optional Chaining
```javascript
console.log(adventurer.cat?.name);

?.이게 옵셔널 체이닝 연산자이다.
이 연산자는 왼쪽 프로퍼티 값이 undefined 또는 null이 아니라면 그다음 프로퍼티 값을 리턴하고,
undefined 또는 null이라면 undefined를 반환한다.

이걸 코드를 통해 동작 원리를 살펴보면 아래와 같다.

console.log((adventurer.cat === null || adventurer.cat === undefined) ? undefined : adventurer.cat.name)

이 옵셔널 체이닝 연산자는 리액트에서 매우 자주 사용된다.

API 호출 결과 처리

const MyComponent = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://api.example.com/users')
      .then((response) => response.json())
      .then((data) => setData(data));
  }, []);

  // data가 null인 경우 컴포넌트 렌더링 오류 발생
  const name = data.user.name;

  // Optional Chaining 사용
  const name = data?.user?.name;

  return (
    <div>
      <h1>{name}</h1>
    </div>
  );
};

조건부 렌더링

const MyComponent = ({ user }) => {
  return (
    <div>
      {user?.name && <h1>{user.name}</h1>}
      {!user?.name && <p>사용자 정보가 없습니다.</p>}
    </div>
  );
};

'JavaScript > JavaScript' 카테고리의 다른 글

밀집 배열과 희소 배열  (1) 2024.02.26
JavaScript에서 접근자 프로퍼티와 캡슐화  (0) 2023.10.11
JS 문자열 비교 연산자  (1) 2023.10.06
호이스팅(Hoisting)  (0) 2023.09.27
sort 메소드  (0) 2023.09.06

리액트 개발을 하다 보면 for 문이나 while 문보다는
Array 객체의 메서드(map, reduce, filter)를 더 자주 사용한다.

이러한 방식을 선언형 프로그래밍(Declarative Programming)이라고 하며
리액트 개발에서는 선언형 프로그래밍 방식이 더 선호된다.
그리고 이와 대비되는 개념은 명령형 프로그래밍(Imperative Programming)이라 한다.

반복문에서의 명령형 프로그래밍과 선언형 프로그래밍

명령형 프로그래밍

명령형 프로그래밍(Imperative Programming)은 기존에 일반적으로 사용되는 프로그래밍 방식이다.

const numbers = [1, 2, 3, 4, 5];
let sum = 0;

// numbers 배열의 짝수 합
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    sum += numbers[i];
  }
}

이처럼 합계를 구하는 코드가 어떻게 동작하는지를 작성하는 방식을 명령형 프로그래밍이라고 한다.
이것만 보고는 명령형 프로그래밍에 대해 감이 잡히지 않을 수 있다.
왜냐하면 명령형 프로그래밍 방식은 우리가 일반적으로 코딩하는 방식이기 때문이다.
아래의 선언형 프로그래밍을 보면 확실하게 이해가 갈 것이다.

선언형 프로그래밍

선언형 프로그래밍(Declarative Programming)은 원하는 결과를 묘사하는 방식으로 코드를 작성한다.
아래의 예제는 위에 명령형 배열의 예에서 사용했던 짝수의 합을 구하는 예제이다.

const numbers = [1, 2, 3, 4, 5];

// 1. filter 함수로 짝수만 추출
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
const evenNumbers = numbers.filter(number => number % 2 === 0);

// 2. reduce 함수로 합산
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
const sum = evenNumbers.reduce((acc, number) => acc + number, 0);

위에 예제는 filter() 함수와 recue() 함수를 사용해 배열의 짝수인 합을 구한다.
여기에서 중요한 것은 어떻게 필터링하고, 어떻게 합을 구하는지가 아니라 결과를 얻는 것에 초점이 맞춰져 있다.
이렇게 원하는 결과가 무엇인지에 초점을 맞추는 방식을 선언현 프로그래밍이라고 한다.

UI에서의 명령형 프로그래밍과 선언형 프로그래밍

명령형 프로그래밍

<div id="root"></div>
const root = document.getElementById("root");
const $inputFiled = document.createElement("input");
const $submitBtn = document.createElement("button");

$submitBtn.textContent = "Send";
$submitBtn.disabled = true;

root.appendChild($inputFiled);
root.appendChild($submitBtn);

$inputFiled.addEventListener("input", (event) => {
  const inputValue = event.target.value;
  $submitBtn.disabled = inputValue.trim().length === 0;
});

$submitBtn.addEventListener("click", () => {
  $inputFiled.value = "";
  $submitBtn.disabled = true;
});

이처럼 명령형 프로그래밍은
DOM 트리의 요소 생성, 속성 설정, 이벤트 추가 등의 단계를
순차적인 명령으로 수행한다.

그리고 상태(state)는 직접 명령($submitBtn.disabled = true;)을 통해 변경한다.
만약 입력한 값을 지우려면,
즉, 상태를 변경하려면 해당 명령을 직접 해야 한다.
이처럼 상태 변경을 매우 직관적으로 수행한다.

선언형 프로그래밍

function Form() {
  const [inputValue, setInputValue] = useState("");

  const handleChange = ((e) => {
    setInputValue(e.target.value);
  });

  const handleClick = (() => {
    setInputValue("");
  })

  const isButtonDisabled = inputValue.trim().length === 0;

  return (
    <div>
      <input type="text" value={inputValue} onChange={handleChange} />
      <button disabled={isButtonDisabled} onClick={handleClick} >
        Send
      </button>
    </div>
  )
}

이처럼 선언형 프로그래밍은
DOM 트리에 태그를 직접 명령하여 생성하는 방식이 아닌
선언만 하면(return ()) 나머지는 리액트가 알아서 처리하는 방식이다.

또한, text = 123처럼 명령형으로 값을 직접 변경할 수 없다.
setText() 함수를 호출해 내부에서 처리한 후
리액트가 컴포넌트에 다시 렌더링 하는 방식이다.

이러한 선언형 프로그래밍은 코드는 간결하며 가독성을 높여준다.
그리고 선언현 프로그래밍은 추상화를 가능하게 하며 이 때문에 재사용성이 높은 특징을 갖게 된다.

 


참고) 추상화란

추상화란 복잡한 것을 간단하게 보여주는 것을 말한다.
예를 들어 '자동차의 엑셀을 밞으면 자동차가 앞으로 간다'처럼,
사실 내부에서는 엔진 동작이나, 기어 등 복잡한 로직들이 구현되어 있지만, 엑셀만 밟으면 앞으로 간다로 필요한 부분만 추출해서 간단하게 만드는 것을 추상화라고 한다.

이런 추상화는 코드의 재사용성을 높여주는데,
만약 '엑셀을 밞으면 앞으로 간다.'를 함수로 정의했다고 하면
버스의 '엑셀을 밞으면 앞으로 간다.'
승용차의 '엑셀을 밟으면 앞으로 간다.'
처럼 필요할 때 '엑셀을 밞으면 앞으로 간다.' 함수만 가져와서 사용하면 되기에
하나의 함수를 다시 사용할 수 있다.
이를 재사용성을 높여 준다고 한다.

  1. 세션과 쿠키의 작동 방식:
    • Django에서 로그인 시, 사용자의 세션 정보는 서버의 데이터베이스에 저장됩니다. 이 세션에는 랜덤으로 생성된 세션 ID가 포함되어 있습니다.
    • 서버는 이 세션 ID를 쿠키에 담아 브라우저로 보냅니다. 브라우저는 이 쿠키를 저장하고, 이후 동일한 도메인으로 요청을 보낼 때마다 이 쿠키를 함께 전송합니다.
  2. 도메인 기반 쿠키:
    • 쿠키는 도메인을 기준으로 작동합니다. 특정 도메인에서 생성된 쿠키는 해당 도메인에 대한 요청 시에만 브라우저에 의해 전송됩니다. 이는 보안과 관련된 중요한 특징입니다.
  3. CORS 설정:
    • 프론트엔드(예: React)와 백엔드(Django)가 다른 도메인 또는 포트에서 실행될 때, 백엔드에 대한 AJAX 요청을 위해 CORS_ALLOWED_ORIGINS 설정이 필요합니다. 이 설정은 백엔드가 어떤 오리진에서 오는 요청을 허용할지 지정합니다.
  4. Axios와 쿠키 전송:
    • JavaScript 또는 Axios를 사용하여 요청을 보낼 때, withCredentials: true 설정을 추가해야 합니다. 이 설정은 AJAX 요청에 쿠키를 포함시키기 위해 필요합니다.
  5. 백엔드 설정:
    • Django 백엔드에서 CORS_ALLOW_CREDENTIALS = True를 설정해야만, AJAX 요청에 포함된 쿠키를 받아들일 준비가 됩니다.

Django와 React에서 세션과 쿠키를 사용한 인증을 구현하기 위한 기본적인 설정 예시를 제공하겠습니다.

Django 설정

django-cors-headers 설치 이후

settings.py에서 CORS 설정

   # settings.p

   INSTALLED_APPS = [
       # ... 기타 설치된 앱들 ...
       'corsheaders',  # corsheaders 앱 추가
       # ... 기타 설치된 앱들 ...
   ]

   MIDDLEWARE = [
       # ... 기타 미들웨어 ...
       'corsheaders.middleware.CorsMiddleware',  # CORS 미들웨어 추가, 꼭 아래의 미들웨어 보다는 먼저 작성
       'django.middleware.common.CommonMiddleware',
       # ... 기타 미들웨어 ...
   ]

   # 모든 오리진을 허용하거나 특정 오리진만 허용
   CORS_ALLOW_ALL_ORIGINS = True
   # 또는
   # CORS_ALLOWED_ORIGINS = [
   #     "http://localhost:3000",  # React 앱의 URL
   # ]

   # AJAX 요청에 쿠키를 포함하기 위해
   CORS_ALLOW_CREDENTIALS = True

React 설정 (Axios 사용 예시)

Axios 인스턴스 생성

   // Axios 설정
   import axios from 'axios';

   const instance = axios.create({
     baseURL: "http://127.0.0.1:8000/api/v1",  // Django 서버 주소
     withCredentials: true,  // 쿠키를 포함시키기 위해
   });

   export default instance;

CSRF

Django에서 CSRF(Cross-Site Request Forgery) 토큰은 중요한 보안 기능 중 하나입니다. CSRF 토큰은 사용자의 세션을 보호하기 위해 사용되며, 사이트 간 요청 위조 공격을 방지하는 데 도움이 됩니다. Django의 CSRF 보호 기능을 사용하려면 다음과 같은 설정이 필요합니다.

Django CSRF 설정

settings.py에서 CSRF 설정

# settings.py

CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:3000"] # front 도메인 주소

템플릿에서 CSRF 토큰 사용

<form method="post">
    {% csrf_token %}
    <!-- 폼 필드 -->
</form>

AJAX 요청에 CSRF 토큰 포함
Django에서는 AJAX 요청에도 CSRF 토큰을 포함해야 합니다. 이를 위해 React 애플리케이션에서 요청을 보낼 때, CSRF 토큰을 요청 헤더에 포함해야 합니다.

React 설정 (Axios 사용 예시)

Axios 인스턴스에 CSRF 토큰 추가

// Axios 설정
import axios from 'axios';

const getCsrfToken = () => {
  return document.cookie.split('; ').find(row => row.startsWith('csrftoken=')).split('=')[1];
};

const instance = axios.create({
  baseURL: "http://127.0.0.1:8000/api/v1",  // Django 서버 주소
  withCredentials: true,  // 쿠키를 포함시키기 위해
  headers: {
    'X-CSRFToken': getCsrfToken(),  // CSRF 토큰 추가
  }
});

export default instance;

위의 코드는 React 앱에서 Django 서버로 보내는 모든 요청에 CSRF 토큰을 포함시키는 방법을 보여줍니다. getCsrfToken 함수는 브라우저의 쿠키에서 CSRF 토큰을 추출하고, Axios 인스턴스는 이 토큰을 요청 헤더에 추가하여 서버에 전송합니다. 이 방법으로 Django 서버의 CSRF 보호 기능을 충족시킬 수 있습니다.

Django는 사용자 인증 및 관리를 위해 여러 유틸리티 함수를 제공합니다. check_password, set_password, authenticate, login, logout 함수들을 예시로 들 수 있습니다.

 

1. check_password()

  • 용도: 사용자의 비밀번호가 입력된 비밀번호와 일치하는지 확인합니다.
  • 예시:
  • from django.contrib.auth.models import User user = User.objects.get(username='myusername') if user.check_password('mypassword'): print("비밀번호가 일치합니다.") else: print("비밀번호가 일치하지 않습니다.")

2. set_password()

  • 용도: 사용자의 비밀번호를 새로 설정합니다.
  • 예시:
    user = User.objects.get(username='myusername')
    user.set_password('newpassword')
    user.save()

3. authenticate()

  • 용도: 사용자 이름과 비밀번호가 유효한지 확인하고, 해당하는 User 객체를 반환합니다.
  • 예시:
  • from django.contrib.auth import authenticate user = authenticate(request, username='myusername', password='mypassword') if user is not None: print("인증에 성공했습니다.") else: print("인증에 실패했습니다.")

4. login()

  • 용도: 주어진 HttpRequest 객체와 User 객체를 사용하여 세션을 시작합니다.
  • 예시:
  • from django.contrib.auth import login def my_view(request): # 사용자 인증이 이미 이루어진 상황 가정 user = authenticate(request, username='myusername', password='mypassword') if user is not None: login(request, user) # 로그인 후 처리

5. logout()

  • 용도: 현재 세션을 종료합니다.
  • 예시:
  • from django.contrib.auth import logout def my_view(request): logout(request) # 로그아웃 후 처리

JavaScript에서 접근자 프로퍼티와 캡슐화

접근자 프로퍼티: Getter와 Setter

JavaScript에서 객체의 프로퍼티에는 크게 두 가지 유형이 있습니다. 데이터 프로퍼티와 접근자 프로퍼티입니다.

데이터 프로퍼티

기본적으로 변수에 값을 할당하는 것처럼 객체의 프로퍼티에 값을 저장하는 것을 '데이터 프로퍼티'라고 부릅니다.

const car = {
  model: "Sedan",
  year: 2022
};

접근자 프로퍼티

반면에, 접근자 프로퍼티는 실제 값을 갖지 않습니다. 대신, 다른 프로퍼티의 값을 읽거나 저장할 때 작동하는 getset 메서드를 정의합니다.

const student = {
  _score: 90,
  get score() {
    return this._score;
  },
  set score(value) {
    if (value < 0 || value > 100) {
      throw new Error("점수는 0~100 사이여야 합니다.");
    }
    this._score = value;
  }
};

클래스에서의 접근자 프로퍼티

getset 메서드는 클래스에서도 사용 가능합니다.

class Movie {
  constructor(title, year) {
    this.title = title;
    this.year = year;
  }
  get info() {
    return `${this.title} (${this.year})`;
  }
  set yearReleased(y) {
    if (y < 1800) return;
    this.year = y;
  }
}

일반적으로는 데이터 프로퍼티와 동일한 접근자 프로퍼티 명을 짓습니다.

class User {
  constructor(name, age) {
    this.name = name;
    this._age = age;  // 내부에서만 사용할 프로퍼티는 보통 '_'로 시작합니다.
  }

  get age() {
    return this._age;
  }

  set age(value) {
    if (value < 0) {
      console.error("나이는 음수가 될 수 없습니다.");
      return;
    }
    this._age = value;
  }
}

const user = new User("John", 30);
console.log(user.age); // 30

user.age = -1;  // "나이는 음수가 될 수 없습니다."
console.log(user.age); // 30

다만, _age라고 변수 명을 수정해야 합니다. 그렇지 않고 age를 사용하면 데이터 프로퍼티에 할당하는 과정(this.age = age)에 age를 호출할 경우 set메서드를 호출하는 과정을 반복할 수도 있습니다.

프로퍼티 이름 앞에 언더스코어(_)를 붙이면 이 프로퍼티는 클래스 내부에서만 사용되고, 외부에서는 접근하지 않는 것으로 알려주긴 하나, 실제로는 외부에서도 접근 가능합니다.

console.log(user._age); // 30

이렇게 접근할 수 있으므로 완전한 캡슐화를 보장하지는 않습니다. 그래서 아래처럼 # 를 사용해 캡슐화를 구현할 수 있습니다.

은닉과 캡슐화

JavaScript에서는 # 기호를 사용하여 private 필드를 정의할 수 있습니다. 이러한 private 필드는 클래스 외부에서 접근할 수 없어 캡슐화를 구현합니다.

class BankAccount {
  #balance = 0;

  get balance() {
    return this.#balance;
  }

  deposit(amount) {
    if (amount < 0) return;
    this.#balance += amount;
  }
}

이 방법을 사용하면, 외부에서 #balance 필드에 직접 접근할 수 없습니다.

const myAccount = new BankAccount();
console.log(myAccount.#balance);  // SyntaxError

'JavaScript > JavaScript' 카테고리의 다른 글

밀집 배열과 희소 배열  (1) 2024.02.26
옵셔널 체이닝(Optional Chaining)  (1) 2024.02.25
JS 문자열 비교 연산자  (1) 2023.10.06
호이스팅(Hoisting)  (0) 2023.09.27
sort 메소드  (0) 2023.09.06

문자열 비교 연산자

javaScript에서 문자열을 비교할 때는 ASCII 코드의 순서로 비교됩니다.

알파벳 비교

Copy code
console.log('apple' < 'banana'); // true
console.log('apple' > 'banana'); // false

대소문자

대문자 ASCII 코드 값이 소문자보다 작습니다.

Copy code
console.log('Apple' < 'apple'); // true
console.log('Apple' > 'apple'); // false

숫자형 문자열

숫자로 구성된 문자열도 사전식으로 비교됩니다.

Copy code
console.log('2' > '12'); // true, 문자열로서 '2'는 '12'보다 사전식으로 뒤에 옴
console.log('12' < '2'); // true, 문자열로서 '12'는 '2'보다 사전식으로 앞에 옴

특수문자

특수문자도 ASCII 코드 값을 기준으로 합니다.

Copy code
console.log('a' > '!'); // true, 'a'의 ASCII 코드 값이 '!'보다 큼

주의점

console.log(
  100 > 12,       //숫자는 그 자체로 비교
  '100' > '12',   //문자는 사전순으로 비교
  '100' > 12,     // 문자와 숫자를 비교하면 문자를 숫자로 변환
)

Django Rest Framework에서 serializer에서 각 필드에 대한 validation 로직을 추가할 수 있습니다. 검증을 위한 메서드 명은 validate_필드이름 라고 작성하고 검증할 로직을 구현하면 됩니다.

간단한 방 정보를 다루는 예시를 통해서 사용법을 설명하겠습니다.

모델

class Room(model.Model):
    name = models.CharField(max_length=100)
    price = models.PositiveIntegerField()
    description = models.TextField()

시리얼라이저

class RoomSerializer(serializers.ModelSerializer):
    class Meta:
        model = Room
        fields = ["name", "price", "description"]

    def validate_name(self, value):
        if len(value) < 3:
            raise serializers.ValidationError("방 이름은 최소 3글자 이상이어야 합니다.")
        return value

    def validate_price(self, value):
        if value < 0:
            raise serializers.ValidationError("가격은 음수일 수 없습니다.")
        return value

이 예시에서는 Room 모델을 정의하고, 이를 시리얼라이저로 변환하는 RoomSerializer를 만들었습니다. RoomSerializerRoom 모델과 연결되어 있습니다.

RoomSerializer 내부에서 validate_name 메소드와 validate_price 메소드를 정의했습니다.

  • validate_name 메소드는 name 필드의 값을 검증합니다. 만약 이름이 3글자 미만이면 serializers.ValidationError을 발생시켜 에러를 일으킵니다.
  • validate_price 메소드는 price 필드의 값을 검증합니다. 만약 가격이 음수라면 마찬가지로 에러를 발생시킵니다.

이런 식으로 각 필드에 대한 검증 로직을 추가할 수 있습니다. 이제 이 시리얼라이저를 사용하면, 입력된 데이터가 검증되고 필요한 처리가 이루어집니다.

serializer = RoomSerializer(data=data) 

if serializer.is_valid(): 
    # 유효한 경우, 데이터를 저장하거나 다른 처리를 할 수 있습니다. 
    room = serializer.save() 
else: 
    # 유효하지 않은 경우, 에러 메시지를 확인할 수 있습니다. 
    errors = serializer.errors

이렇게 하면 시리얼라이저를 통해 데이터를 검증하고 처리할 수 있습니다.

호이스팅(Hoisting)의 정의

호이스팅은 JavaScript에서 변수와 함수의 선언을 현재 스코프의 최상단으로 끌어올리는 특성을 말합니다. 이는 코드 실행 전에 JavaScript 엔진이 변수와 함수의 선언을 메모리에 할당하기 때문에 발생합니다.

호이스팅의 작동 원리

JavaScript의 실행 컨텍스트는 크게 세 단계로 나뉩니다

  1. 생성 단계 (Creation Phase)
    • 변수는 undefined로 초기화됩니다.
    • 함수 선언은 메모리에 저장되며, 해당 스코프의 최상단으로 끌어올려집니다.
  2. 실행 단계 (Execution Phase)
    • 코드가 줄 단위로 실행됩니다.
    • 변수에 값이 할당되고 함수가 호출됩니다.
  3. 종료 단계 (Termination Phase)

호이스팅은 생성 단계에서 발생합니다.

예)

console.log(a); // undefined
var a = 5;
console.log(a); // 5

foo(); // "Hello from foo!"
function foo() {
    console.log("Hello from foo!");
}

이 코드에서는 호이스팅 때문에 첫 번째 console.log(a);에서 에러가 발생하지 않고 undefined가 출력됩니다. 또한, 함수 foo가 선언되기 전에 호출되었지만, 호이스팅 덕분에 문제없이 실행됩니다.

변수 선언과 호이스팅

var로 선언된 변수의 호이스팅

  • var로 선언된 변수는 호이스팅 될 때 초기값이 undefined로 설정됩니다.
  • 변수 선언 및 초기화 이전에 변수를 참조하면 undefined 값을 반환합니다.
console.log(varVariable); // undefined
var varVariable = 5;
console.log(varVariable); // 5

let로 선언된 변수의 호이스팅

  • let로 선언된 변수도 호이스팅되지만, var와는 다르게 초기화 단계에서 undefined로 설정되지 않습니다.
  • 변수 선언 이전에 변수를 참조하려고 시도하면 "ReferenceError"가 발생합니다. 이 시기를 Temporal Dead Zone (TDZ)라고 부릅니다.
    console.log(letVariable); 
    // ReferenceError: Cannot access 'letVariable' before initialization
    let letVariable = 10;
    console.log(letVariable); // 10

const로 선언된 변수의 호이스팅

  • const로 선언된 변수도 호이스팅 되며, let과 마찬가지로 초기화 단계에서 undefined로 설정되지 않습니다.
  • 변수 선언 및 초기화 이전에 변수를 참조하려고 시도하면 "ReferenceError"가 발생합니다. 마찬가지로 이 시기는 Temporal Dead Zone (TDZ)에 속합니다.
  • const는 선언과 동시에 초기화가 반드시 이루어져야 합니다.
console.log(constVariable); 
// ReferenceError: Cannot access 'constVariable' before initialization
const constVariable = 20;
console.log(constVariable); // 20

정리

  • var: 변수가 호이스팅 되며 undefined로 초기화됩니다.
  • let: 변수가 호이스팅되나 초기값이 없으며 TDZ에 속하게 됩니다.
  • const: 변수가 호이스팅되며 초기값이 필요하고, TDZ에 속하게 됩니다.

함수 선언과 호이스팅

함수 선언식(Function Declarations)

  • 함수 선언식은 호이스팅 될 때 해당 스코프의 최상단으로 완전히 끌어올려집니다.
  • 이는 함수의 이름과 함께 함수의 본문도 함께 끌어올려진다는 것을 의미합니다.
  • 그 결과, 함수 선언 전에 함수를 호출할 수 있습니다.
// 호이스팅으로 인해 함수 선언 전에도 호출 가능
helloFunction();
// 안녕하세요!

function helloFunction() {
    console.log("안녕하세요!");
}

함수 표현식 (Function Expressions)

  • 함수 표현식은 변수에 함수를 할당하는 방식으로 정의됩니다.
  • 함수 표현식에 사용된 변수는 호이스팅 될 때 undefined로 초기화됩니다.
  • 그러나 함수 본문은 호이스팅되지 않습니다.
  • 따라서 함수 표현식을 통해 정의된 함수는 변수가 초기화된 후에만 호출할 수 있습니다.
// 호이스팅 때문에 변수는 이미 존재하지만, 함수의 본문은 아직 할당되지 않았으므로 에러 발생
helloExpression(); 
// TypeError: helloExpression is not a function

var helloExpression = function() {
    console.log("안녕하세요!");
};

helloExpression(); 
//안녕하세요!

알겠습니다. 화살표 함수에 대한 호이스팅의 동작을 자세히 알아보겠습니다.

화살표 함수 (Arrow Functions)

  • 화살표 함수는 함수 표현식의 간결한 버전으로 생각할 수 있습니다.
  • 화살표 함수는 변수에 할당되므로, 변수의 호이스팅 규칙이 적용됩니다.
  • 따라서 화살표 함수 자체는 호이스팅되지 않습니다. 하지만, 해당 화살표 함수가 할당된 변수는 호이스팅 될 때 undefined로 초기화됩니다.
  • 그 결과, 변수 할당 이전에 화살표 함수를 호출하려고 하면 오류가 발생합니다.
// 호이스팅 때문에 변수는 이미 존재하지만, 함수의 본문은 아직 할당되지 않았으므로 에러 발생
helloArrow(); 
// TypeError: helloArrow is not a function

var helloArrow = () => {
    console.log("안녕하세요!");
};

helloArrow(); 
// "안녕하세요!

참고: let 또는 const와 함께 선언된 화살표 함수의 경우, 호이스팅이 발생하지만 Temporal Dead Zone (TDZ) 에 있어 호출 시 "ReferenceError"가 발생합니다.

helloArrowWithLet(); 
// ReferenceError: Cannot access 'helloArrowWithLet' before initialization

let helloArrowWithLet = () => {
    console.log("let으로 선언된 화살표 함수입니다.");
};

helloArrowWithLet(); 
// let으로 선언된 화살표 함수입니다.

정리

  • 함수 선언식: 함수의 전체 본문이 호이스팅 되므로 선언 전에 호출할 수 있습니다.
  • 함수 표현식: 변수는 호이스팅되나 함수 본문은 호이스팅 되지 않으므로, 본문 할당 전에는 함수로서 호출할 수 없습니다.
  • 화살표 함수: 변수의 호이스팅 규칙이 적용되므로, 화살표 함수 본문은 호이스팅 되지 않습니다. 변수는 호이스팅 되나 초기화 전에 함수로서 호출하려면 오류가 발생합니다.

호이스팅의 범위

전역 범위 (Global Scope)에서의 호이스팅

  • 전역 범위에서 선언된 변수나 함수는 전역 객체 (브라우저에서는 window 객체)에 바인딩됩니다.
  • 호이스팅이 발생하면, 이러한 변수나 함수 선언은 스크립트의 최상단으로 끌어올려집니다.
  • 이는 앞에서 작성한 일반적인 호이스팅입니다.
console.log(globalVar); // undefined
var globalVar = "전역 변수";
console.log(globalVar); // "전역 변수"

globalFunction(); // "전역 함수 호출됨"

function globalFunction() {
    console.log("전역 함수 호출됨");
}

함수 범위 (Function Scope)에서의 호이스팅

  • 함수 내부에서 선언된 변수나 함수는 해당 함수 범위 내에서만 접근 가능합니다.
  • 함수 범위에서의 호이스팅은 해당 함수의 최상단으로 변수나 함수 선언을 끌어올립니다.
function exampleFunction() {
    console.log(localVar); // undefined
    var localVar = "함수 내부의 변수";
    console.log(localVar); // "함수 내부의 변수"

    localFunction(); // "함수 내부의 함수 호출됨"

    function localFunction() {
        console.log("함수 내부의 함수 호출됨");
    }
}

exampleFunction();
  • 또한, 함수 내부에서 var로 선언된 변수는 해당 함수의 전체 범위에서 사용할 수 있습니다. 이는 함수 범위의 호이스팅 때문입니다.
function hoistingExample() {
    console.log(functionScopedVar); // undefined
    if (true) {
        var functionScopedVar = "변수";
    }
    console.log(functionScopedVar); // "변수"
}

hoistingExample();

위에 예제에서 var functionScopedVar = "변수"if 블록 내부에서 선언되었지만, 함수의 어디에서나 접근할 수 있습니다. 이는 var가 함수 범위를 가지고, 호이스팅이 적용되기 때문입니다.

만약 조건절이 false인 경우는 var로 선언된 변수의 초기화 코드는 실행되지 않습니다. 하지만 변수 자체는 여전히 호이스팅되어 함수의 최상단에 선언되므로, undefined 값으로 초기화 됩니다.

function hoistingExample() {
    console.log(functionScopedVar); // undefined
    if (false) {
        var functionScopedVar = "변수";
    }
    console.log(functionScopedVar); // undefined
}

hoistingExample();

이러한 특성 때문에 var는 함수 내에서 예측하기 어려운 코드가 될 수 있습니다.
반면, letconstvar와 다르게 블록 범위(block scope)를 가집니다.

블록 범위(Block Scope)와 let, const

  • var는 함수 범위를 가지며, letconst는 블록 범위를 가집니다.
  • letconst로 선언된 변수는 호이스팅 되나, 초기화 전에 접근하려고 하면 Temporal Dead Zone (TDZ)에서 오류가 발생합니다.
function blockScopeExample() {
    if (true) {
        console.log(blockScopedVar); 
        // ReferenceError: Cannot access 'blockScopedVar' before initialization
        let blockScopedVar = "블록 범위의 변수";
        console.log(blockScopedVar); // "블록 범위의 변수"
    }
    console.log(blockScopedVar); 
    // ReferenceError: blockScopedVar is not defined
}

blockScopeExample();

TDZ(Temporal Dead Zone)

letconst로 선언된 변수도 호이스팅은 발생합니다. 다만, 이 변수들은 Temporal Dead Zone (TDZ) 시기에 접근할 수 없습니다. 왜냐하면, letconst가 블록 범위(block scope)를 가지며, 선언과 초기화가 분리되어 이루어지기 때문입니다.

Temporal Dead Zone (TDZ)

  • Temporal Dead Zone (TDZ)는 변수가 코드에서 물리적으로 선언된 위치와 초기화(할당)된 위치 사이의 코드 영역을 지칭합니다.
  • 이 시간 동안 해당 변수에 접근하려고 하면 JavaScript는 "ReferenceError"를 던집니다.
// 호이스팅
// TDZ 시작
console.log(myLetVariable); 
// ReferenceError: Cannot access 'myLetVariable' before initialization
// TDZ 끝, 초기화 시작
let myLetVariable = 10;

console.log(myLetVariable); // 10

letconst 선언도 호이스팅 되지만, 호이스팅 되는 시점과 초기화되는 시점 사이에 TDZ가 존재합니다.

  1. 호이스팅: 변수 선언이 스코프의 최상단으로 끌어올려집니다.
  2. TDZ 시작: 변수 선언문을 코드 실행이 만나면 TDZ가 시작됩니다.
  3. TDZ 끝 & 초기화: 변수 초기화(할당)가 실행되면 TDZ가 끝나고 변수가 사용 가능해집니다.

비교: var, let, const

  • var: 함수 범위를 가지며 호이스팅 됩니다. 호이스팅 된 변수는 undefined로 초기화됩니다.
  • let: 블록 범위를 가지며 호이스팅 되나, TDZ에 있을 때에는 접근할 수 없습니다.
  • const: 블록 범위를 가지며 호이스팅되나, TDZ에 있을 때에는 접근할 수 없습니다. 초기화 없이 선언이 불가능합니다.

변수는 일반적으로 데이터를 저장할 수 있는 메모리 공간을 의미합니다.

다만, 파이썬에서의 변수는 C, 자바 같은 컴파일 언어에서의 변수와는 차이가 있습니다.

자바의 변수

자바를 예로 들면 자바는 변수를 선언하면서 선언된 자료형 만큼의 메모리를 확보합니다. 더 정확하게 말하면 원시 타입(Primitive Type)에 해당하는 변수(int, double, boolean 등)를 선언하면 선언된 타입만큼의 크기의 영역을 Stack 메모리에 선언합니다. 그리고 객체와 메서드 같은 참조형 타입(Reference type)은 Heap 메모리에 실질적인 데이터가 저장되고 그 데이터의 주소값이 stack 메모리에 저장되는 형태입니다.

파이썬의 변수

하지만 파이썬의 변수는 다릅니다.

파이썬에서 변수는 타입에 의해 선언되는 게 아닌 객체와 연관되어 있습니다.

왜냐하면 변수에 선언되는 데이터는 모두 객체이기 때문입니다.

만약 a = 10 이라는 python 구문이 있다면, 파이썬은 우선 10을 나타내는 객체를 생성합니다. 이후 변수 a를 생성하고 변수 a와 객체 10을 연결합니다.

즉, a와 객체 10은 다른 위치에 저장되고 이 둘은 참조에 의해 연결됩니다.

변수명 참조 주소 메모리 주소 객체 값

a 0x1 ➡️ 0x1 10

사실 이 부분을 좀 더 자세히 설명하면 다음과 같습니다.

interning

파이썬에서는 정수와 같은 몇몇 흔한 값들에 대해 미리 객체를 생성하고 재사용합니다. 이를 "interning"이라고 합니다. 특히 작은 정수에 대해 이런 방식이 적용됩니다.

구체적으로, -5부터 256까지의 정수 객체는 파이썬이 시작될 때 미리 생성되어 있습니다. 따라서 a = 10 구문을 실행할 때, 새로운 10 객체를 생성하는 것이 아니라 이미 생성되어 있는 10 객체를 재사용합니다.

따라서, a = 10을 실행하면 다음과 같은 순서로 동작합니다:

  1. 10 객체가 이미 메모리에 있는지 확인합니다.
  2. 이미 있으면 그 객체의 참조를 변수 a에 연결합니다.
  3. 없으면 (예를 들어, 매우 큰 숫자나 5 이하의 숫자에서는) 새로운 객체를 생성하고 그 참조를 변수 a에 연결합니다.

즉, a = 10 구문을 실행하면 a는 이미 존재하는 10 객체를 참조하게 됩니다.

10과 10은 같은 객체

앞에서 말한 interning은 파이썬 구문으로 확인 가능합니다.

파이썬에서 == 는 값이 같은지 확인하고 'is'는 같은 객체를 참조하고 있는지를 확인해 줍니다.

a = 10
b = 10
a == b
#--> True
a is b
#--> True

10은 같은 객체임을 확인할 수 있습니다.

얕은 복사, 깊은 복사

list_1 = [1,2,3]
list_2 = list_1
list_1[0] = 10
list_2
#--> [10,2,3]

list_1과 list_2는 같은 [1,2,3] 객체를 참조합니다.

그리고 list는 가변 객체이기에 값이 변할 수 있습니다.

List_1[0] = 10 이 구문은 list_1이 다른 객체를 참조하도록 하는 구문이 아니라, list_1이 참조하는 객체 자체를 바꾼 것입니다.

그러기에 같은 객체를 참조하는 list_2의 출력값도 바뀌는 것입니다.

표로 표현하면 다음과 같습니다.

변수명 참조 주소 메모리 주소 객체 값

list_1 0x1 ➡️ 0x1 [1,2,3]
list_2 0x1 ↗️    

list_1[0] = 10 구문 실행

변수명 참조 주소 메모리 주소 객체 값

list_1 0x1 ➡️ 0x1 [10,2,3]
list_2 0x1 ↗️    

다른 예시를 더 살펴보면

동적 타입 언어(dynamic typing)

a = 10
a = "ten"

이 경우 a의 타입이 정수타입에서 문자열타입으로 변경된 것이 아니라 변수 a가 객체 10을 참조했다가, 객체 "ten"을 참조하는 것이다.

왜냐하면 파이썬에서의 변수는 동적으로 타입을 갖습니다. 파이썬의 변수는 참조하는 객체의 타입에 따라 동적으로 타입이 바뀔 수 있습니다.

변수는 사실상 특정 객체를 참조하고 있기에 가능한 것입니다. 자바나, c에서는 불가능합니다. 왜냐하면 변수의 타입에 따라 메모리의 크기를 정했기 때문입니다.

객체 메모리 구성

파이썬의 객체는 메모리 상에서 여러 부분으로 구성되어 있습니다. 헤더에는 타입 정보와 다른 메타데이터가 저장되며, 이후에 실제 데이터 값 등이 저장됩니다.

객체는 타입 정보를 헤더에 저장합니다. 그래서 파이썬에서는 변수에 타입을 지정할 필요가 없습니다. 왜냐하면 각 객체가 자신의 타입 정보를 직접 포함하고 있기 때문입니다. 이 특징 덕분에 앞에서 말한 파이썬이 동적 타이핑 언어가 가능한 이유입니다.

객체의 메모리 구성

타입 정보 (헤더) (Type Information)
참조 카운트 (Reference Count)
객체의 실제 데이터 값 (Object’s Actual Data Value)

참조 카운트

참조 카운트는 파이썬의 메모리 관리 메커니즘 중 하나로, 특정 객체에 대한 참조의 수를 나타냅니다.

파이썬에서는 객체의 메모리를 효율적으로 관리하기 위해 참조 카운팅 방식을 사용합니다. 객체가 생성될 때 해당 객체의 참조 카운트는 1로 설정됩니다. 만약 다른 변수나 데이터 구조가 그 객체를 참조하게 되면 참조 카운트가 증가하며, 참조가 사라질 때마다 참조 카운트가 감소합니다.

참조 카운트가 0이 되면, 해당 객체는 더 이상 어떤 변수나 데이터 구조에도 참조되지 않는 것을 의미합니다. 이 시점에서 파이썬의 가비지 컬렉터(garbage collector)는 해당 객체의 메모리를 해제합니다.

a = [1, 2, 3]  # 리스트 객체 생성, 참조 카운트 = 1
b = a          # 리스트 객체에 대한 참조 추가, 참조 카운트 = 2

del a          # 리스트 객체에 대한 참조 제거, 참조 카운트 = 1
del b          # 리스트 객체에 대한 참조 제거, 참조 카운트 = 0 (이제 객체의 메모리가 해제될 수 있음)

 

'Python > Python' 카테고리의 다른 글

deque 모듈  (0) 2023.08.30
Python - isinstance()  (0) 2023.08.12
Python) 위치 인자, 키워드 인자  (0) 2023.07.25
@classmethod 와 @staticmethod  (0) 2023.07.20
@property 데코레이터  (0) 2023.07.01