- 밀집 배열과 희소 배열 2024.02.26
- 옵셔널 체이닝(Optional Chaining) 2024.02.25
- 명령형 프로그래밍과 선언형 프로그래밍 2024.02.12
- 세션과 쿠키를 이용한 Django 인증 시스템 2023.12.06
- Django의 사용자 인증 및 관리를 위한 유틸리티 함수 2023.11.26
- JavaScript에서 접근자 프로퍼티와 캡슐화 2023.10.11
밀집 배열(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가 아님을 확인하고 접근해야 에러를 방지할 수 있다.
이를 해결하기 위해서는 두개의 방식이 사용된다.
- 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() 함수를 호출해 내부에서 처리한 후
리액트가 컴포넌트에 다시 렌더링 하는 방식이다.
이러한 선언형 프로그래밍은 코드는 간결하며 가독성을 높여준다.
그리고 선언현 프로그래밍은 추상화를 가능하게 하며 이 때문에 재사용성이 높은 특징을 갖게 된다.
참고) 추상화란
추상화란 복잡한 것을 간단하게 보여주는 것을 말한다.
예를 들어 '자동차의 엑셀을 밞으면 자동차가 앞으로 간다'처럼,
사실 내부에서는 엔진 동작이나, 기어 등 복잡한 로직들이 구현되어 있지만, 엑셀만 밟으면 앞으로 간다로 필요한 부분만 추출해서 간단하게 만드는 것을 추상화라고 한다.
이런 추상화는 코드의 재사용성을 높여주는데,
만약 '엑셀을 밞으면 앞으로 간다.'를 함수로 정의했다고 하면
버스의 '엑셀을 밞으면 앞으로 간다.'
승용차의 '엑셀을 밟으면 앞으로 간다.'
처럼 필요할 때 '엑셀을 밞으면 앞으로 간다.' 함수만 가져와서 사용하면 되기에
하나의 함수를 다시 사용할 수 있다.
이를 재사용성을 높여 준다고 한다.
'JavaScript > React' 카테고리의 다른 글
| Class component와 Function component차이, Props와 State 차이 (0) | 2022.12.28 |
|---|
- 세션과 쿠키의 작동 방식:
- Django에서 로그인 시, 사용자의 세션 정보는 서버의 데이터베이스에 저장됩니다. 이 세션에는 랜덤으로 생성된 세션 ID가 포함되어 있습니다.
- 서버는 이 세션 ID를 쿠키에 담아 브라우저로 보냅니다. 브라우저는 이 쿠키를 저장하고, 이후 동일한 도메인으로 요청을 보낼 때마다 이 쿠키를 함께 전송합니다.
- 도메인 기반 쿠키:
- 쿠키는 도메인을 기준으로 작동합니다. 특정 도메인에서 생성된 쿠키는 해당 도메인에 대한 요청 시에만 브라우저에 의해 전송됩니다. 이는 보안과 관련된 중요한 특징입니다.
- CORS 설정:
- 프론트엔드(예: React)와 백엔드(Django)가 다른 도메인 또는 포트에서 실행될 때, 백엔드에 대한 AJAX 요청을 위해
CORS_ALLOWED_ORIGINS설정이 필요합니다. 이 설정은 백엔드가 어떤 오리진에서 오는 요청을 허용할지 지정합니다.
- 프론트엔드(예: React)와 백엔드(Django)가 다른 도메인 또는 포트에서 실행될 때, 백엔드에 대한 AJAX 요청을 위해
- Axios와 쿠키 전송:
- JavaScript 또는 Axios를 사용하여 요청을 보낼 때,
withCredentials: true설정을 추가해야 합니다. 이 설정은 AJAX 요청에 쿠키를 포함시키기 위해 필요합니다.
- JavaScript 또는 Axios를 사용하여 요청을 보낼 때,
- 백엔드 설정:
- Django 백엔드에서
CORS_ALLOW_CREDENTIALS = True를 설정해야만, AJAX 요청에 포함된 쿠키를 받아들일 준비가 됩니다.
- Django 백엔드에서
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 보호 기능을 충족시킬 수 있습니다.
'Python > Django' 카테고리의 다른 글
| Django의 사용자 인증 및 관리를 위한 유틸리티 함수 (1) | 2023.11.26 |
|---|---|
| DRF) serializer에서 validation 메소드 만들기 (0) | 2023.10.01 |
| Django 소유자 여부 확인(DRF) (0) | 2023.08.28 |
| Serializer에서 method 사용하기 (0) | 2023.08.27 |
| Django에서의 Transactions (0) | 2023.08.26 |
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) # 로그아웃 후 처리
'Python > Django' 카테고리의 다른 글
| 세션과 쿠키를 이용한 Django 인증 시스템 (0) | 2023.12.06 |
|---|---|
| DRF) serializer에서 validation 메소드 만들기 (0) | 2023.10.01 |
| Django 소유자 여부 확인(DRF) (0) | 2023.08.28 |
| Serializer에서 method 사용하기 (0) | 2023.08.27 |
| Django에서의 Transactions (0) | 2023.08.26 |
JavaScript에서 접근자 프로퍼티와 캡슐화
접근자 프로퍼티: Getter와 Setter
JavaScript에서 객체의 프로퍼티에는 크게 두 가지 유형이 있습니다. 데이터 프로퍼티와 접근자 프로퍼티입니다.
데이터 프로퍼티
기본적으로 변수에 값을 할당하는 것처럼 객체의 프로퍼티에 값을 저장하는 것을 '데이터 프로퍼티'라고 부릅니다.
const car = {
model: "Sedan",
year: 2022
};
접근자 프로퍼티
반면에, 접근자 프로퍼티는 실제 값을 갖지 않습니다. 대신, 다른 프로퍼티의 값을 읽거나 저장할 때 작동하는 get과 set 메서드를 정의합니다.
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;
}
};
클래스에서의 접근자 프로퍼티
get과 set 메서드는 클래스에서도 사용 가능합니다.
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 |