S1U10 정리

이유정·2022년 9월 6일
0

코드스테이츠 TIL

목록 보기
13/62
post-thumbnail

원시 자료형과 참조 자료형

number, string, boolean과 같은 고정된 저장 공간을 차지하는 데이터를 모두 원시 자료형(primitive data type), 대량의 데이터를 다루기에 적합한 배열과 객체는 참조 자료형(reference data type)이라고 분류. 이런 분류는 데이터를 저장하는 방식에 따뤄 나눴다.

참조 자료형인 배열과 객체는 원시 자료형과 달리 저장 공간이 계속 늘어날 수도 있을 것 같다는 생각이 든다. 그렇다면 어떻게 저장 공간이 유동적으로 늘어날 수 있을까? 그 이유는 배열과 객체, 그리고 함수가 담겨있는 저장 공간은 특별한 저장 공간을 사용하기 때문이다.

원시 자료형이 할당될 때는 변수에 값(value) 자체가 담기고, 참조 자료형이 할당될 때는 보관함의 주소(reference)가 담긴다.
참조 자료형은 기존에 고정된 크기의 보관함이 아니라, 동적으로 크기가 변하는 특별한 보관함을 사용한다는 것을 이해할 수 있다.

primitive data

  • number
  • string
  • boolean
  • undefined
  • null

: 데이터를 stack 에 저장
: 각 변수간의 원시타입 데이터를 복사할 경우에 데이터 값이 복사되기 때문에 기존의 데이터에 영향이 가지 않는다.

reference type data

  • array
  • object
  • function

: stack 에 저장 , stack과 heap의 연결
(값을 저장하는 대신 데이터가 담긴 저장소의 주소를 저장한다.)
: 참조타입은 주소를 복사하기 때문에 복사한 데이터에서 원소를 변경하면 주소안에 있는 데이터가 변경, 기존 데이터에 영향이 가게된다.

원시자료형

원시 자료형은 모두 "하나"의 정보, 즉, 데이터를 담고 있습니다.
데이터 보관함 한 칸에 하나의 데이터만 넣을 수 있는 그때의 "원시적인" 방식. 원시 자료형은 "하나"의 의미를 가지는 데이터임에는 변함이 없습니다. 그렇기 때문에 원시 자료형이 담기는 보관함의 크기는 고정하는 것이 합당합니다. 원시 자료형은 값 자체에 대한 변경이 불가능(immutable)하지만, 변수에 다른 데이터를 할당할 수는 있습니다.

참조자료형

왜 따로 자료 구조를 구현해야만 했을까? 변수에 넣을 수 있는 데이터 크기가 제한되기 때문. 따라서 데이터의 크기가 동적으로 변하는" 특별한 데이터 보관함이 필요해졌다. 이 데이터가 위치한 곳(메모리 상 주소)을 가리키는 주소가 변수에 저장된다. 즉, 변수에는 특별한 데이터 보관함을 찾아갈 수 있는 주소가 담겨있고, 이 주소를 따라가 보면 특별한 데이터 보관함을 찾을 수 있는데, 이 특별한 데이터 보관함에서는 자기 마음대로 사이즈를 늘렸다가 줄였다가한다. ("동적(dynamic)으로 변한다"라고 하기도 함.) 이처럼 데이터는 별도로 관리되고, 우리가 직접 다루는 변수에는 주소가 저장되기 때문에 reference data type이라고 불린다. 이런 특별한 데이터 보관함을 heap이라고도 부른다.

컴퓨터 공학에서는 변수가 가리키고(refer) 있는 데이터의 참조한다는 의미로 사용된다. 읽는 것이 아니라, 그 변수의 주소를 "참조"하여 실제 변수가 있는 장소에 어떤 데이터가 있는지 도착하고 나서야 비로소 "읽을 수" 있기 때문이다.

배열과 객체는 대량의 데이터를 쉽게 다루기 위해서 사용되는데 크기가 상황에 따라서 커졌다가 작아지는 특별한 데이터 저장소를 만들어 사용하기로 합의했다. 변수에는 원시 값 혹은 주소만 지정할 수 있고, 주소는 크기가 변하는 특별한 데이터 저장소를 참조하게 되는 것.

짚고 넘어갈것)

1)

let first = [10, 20, 30, 40];
let second = first;
second[0] = 5;
  • first === second 의 결과는 true이다.

  • first의 0번째 인덱스에 있는 요소는 5이다.

  • first와 second는 같은 주소를 공유하고 있다.

  • first와 second는 참조 자료형이다.

2)

console.log([1,2,3] === [1,2,3]);
 false

참조 자료형의 ===(strict equality)는 주소값이 같은지를 확인합니다. 그렇기 때문에 두 참조 자료형의 주소값은 다르다고 판단을 내릴 수 있습니다.
배열([])과 객체({}) 등 참조 자료형을 읽을 때, 미리 주소값과 메모리 값을 잡아둔다고 생각하면 편합니다. 그러므로 [] === [] 도 true가 나오지 않는 것

3)

let myArray = [2, 3, 4, 5];
let ourArray = myArray;
ourArray[2] = 25;
ourArray = undefined;

ourArray = undefined;
이제 변수 ourArray에 원시 자료형 undefined가 할당되었기 때문에, myArray에 접근할 수 없습니다. 하지만 myArray는 그대로 [2, 3, 25, 5]이다.

4)

let score = 80;
function doStuff(value) {
  value = 90;
}

doStuff(score)

함수 doStuff에 score의 값을 인자로 전달하여 실행합니다.
매개변수 value에 score의 값 80이 전달되고,value = 90에서 value에 90이 할당된다.

다만 변수 score의 값 80은, 참조 자료형이 아니기 때문에 주소값을 전달하지 않고, 값 자체를 복사하여 전달하게 됩니다. 그래서 함수에서 어떤 일이 발생했던가와 관련이 없이 score는 초기에 할당된 값 80이 그대로 유지됩니다.

스코프

무언가 제한된 범위를 잘 들여다보기 위해 사용되는 개념
스코프는 "변수의 유효범위"

Chapter2-1. 스코프와 주요 규칙

중괄호(블록) 안쪽에 변수가 선언되었는가, 바깥쪽에 변수가 선언되었는가가 중요합니다. 이 범위를 우리는 스코프라고 부릅니다.

greeting 변수는 바깥에 정의되어 있으므로, 함수 안쪽에서 사용할 수 있습니다.
firstName 변수는 함수 안쪽에 정의되어 있으므로 함수 바깥쪽에서는 접근이 불가능합니다. 따라서 ReferenceError를 냅니다.

변수의 접근 범위가 이번엔 함수에 의해 나누어졌다.
안쪽 스코프, 바깥쪽 스코프

첫번째 규칙.
-> 바깥쪽 스코프에서 선언한 변수는 안쪽 스코프에서 사용 가능
-> 안쪽 스코프에서 선언한 변수는 바깥쪽 스코프에서 사용 불가능

두번째 규칙.
스코프는 중첩이 가능하다.
바깥쪽의 스코프는 전역 스코프라고 부른다. 전역의 반대말은 지역으로 전역이 아닌 다른 스코프는 모두 '지역 스코프'라고 한다.
지역 스코프에서 선언한 변수는 지역 변수
전역 스코프에서 선언한 변수는 전역 변수다.
지역 변수는 전역 변수보다 더 높은 우선순위를 가진다.

1)

let name = '김코딩';

function showName() {
 let name = '박해커'; // 지역 변수
 console.log(name); // 두 번째 출력
}

console.log(name); // 첫 번째 출력
showName();
console.log(name); // 세 번째 출력

첫 번째 출력은 첫째 줄에서 전역 변수로 선언된 name을 가져옵니다. 이는, showName 함수 안쪽에 선언된 지역 변수 name은 애초에 스코프 규칙에 의해 접근할 수 없기 때문입니다. 따라서 "김코딩"을 출력합니다.

반면, 두 번째 출력은 함수 안에서 선언한 name이라는 지역 변수에 접근하고 있습니다. 변수 이름이 전역 변수와 똑같지만, 지역 변수가 전역 변수보다 우선순위가 높으므로, 지역 변수 name이 출력되는 것입니다. 동일한 변수 이름으로 인해 바깥쪽 변수가 안쪽 변수에 의해 가려지는(shadow) 이러한 현상을 쉐도잉(variable shadowing)이라고 부릅니다.

세 번째 출력은 첫 번째 출력과 마찬가지로 전역 변수 name을 출력합니다. 지역 변수에 선언된 name 변수는 안쪽 스코프이므로 접근이 불가능합니다. 따라서 "김코딩"을 출력합니다.

2)

let name = '김코딩';

function showName() {
  name = '박해커';
  console.log(name); // 두 번째 출력
}

console.log(name); // 첫 번째 출력
showName();
console.log(name); // 세 번째 출력

앞서 문제와는 다르게, 세 번째 줄에서 let 키워드를 사용한 선언이 존재하지 않습니다.

이는, '박해커'라는 값으로 할당하고 있는 name 변수는 전역에 선언된 name 변수를 그대로 사용하겠다는 의미입니다. 지역 스코프에서 새로 선언되지 않으면 그냥 같은 변수입니다.

따라서 showName 함수가 실행되기 전, 처음에는 '김코딩'을 출력하고, 그 이후에는 전역변수 name의 값이 바뀌기 때문에 두 번째 및 세 번째 출력에 '박해커'가 출력됩니다.

Chapter2-2. 변수 선언과 스코프

먼저 스코프는 두 가지 종류가 있습니다.
1. 블록 스코프(block scope)
중괄호를 기준으로 범위가 구분됩니다.

또 다른 스코프 종류로는 2. 함수 스코프(function scope)가 있습니다. function 키워드가 등장하는 함수 선언식 및 함수 표현식은 함수 스코프를 만듭니다.

여기서 한 가지 유의해야 할 점.
화살표 함수는 블록 스코프로 취급됩니다. 함수 스코프가 아닙니다.

1)

for(let i=0; i<5; i++){
console.log(i) ; 
}
console.log('final i:', i)// ? 

블록 스코프 안에서 정의된 변수 i는 블록 범위를 벗어나는 즉시 접근할 수 없습니다.
따라서 결과로는 ReferenceError가 나오게 됩니다.

2)

for(var i=0; i<5; i++){
console.log(i) ; 
}
console.log('final i:', i)// ? 

이에 대한 결과는 5입니다. 어떻게 블록을 벗어났음에도 불구하고 변수 i에 접근할 수 있는 것일까요?
결론부터 말하면, var 키워드는 for 문이 만들어낸 블록 스코프를 무시합니다. var 키워드로 정의한 변수는 블록 스코프를 무시하고, 함수 스코프만 따릅니다. 그러나, 모든 블록 스코프를 무시하는 건 아닙니다. 화살표 함수의 블록 스코프는 무시하지 않습니다. var는 이 규칙을 무시하므로, 코드를 작성하는 사람이 블록 스코프/함수 스코프에 대한 이해가 없으면 코드가 다소 혼란스러울 수 있습니다.
var 키워드보다 let 키워드가 안전한 이유는 또 있습니다.
let 키워드는 재선언을 방지합니다. 실제로 코딩할 때에 변수를 재선언해야 할 필요가 있을까요? 대부분 이런 경우는 버그입니다.

따라서, var 보다는 let 으로 변수 선언을 하는 것을 권장

const 라는 키워드도 있습니다. 변하지 않는 값, 곧 상수(constant)를 정의할 때에는 const를 이용합니다. const는 값의 재할당이 불가능합니다. 값을 재할당할 경우 TypeError를 내므로, 의도하지 않은 값의 변경을 막을 수 있습니다.

<정리>
let 유효범위 : 블록 스코프 및 함수 스코프
const 유효범위: 블록 스코프 및 함수 스코프
var 유효범위: 함수 스코프

let : 값 재할당 불가능
const : 값 재할당 불가능
var : 값 재할당 가능

let : 재선언 불가능
const : 재선언 불가능
var : 재선언 가능

Chapter2-3. 변수 선언할 때 주의할 점

먼저 브라우저에만 존재하는 window 객체에 대해 알아봅시다.

지금 개발자 도구를 열어 콘솔에 window 라고 입력해 보세요. 객체 하나를 조회할 수 있습니다. 이 객체는 사실 브라우저의 창(window)을 의미하는 객체이지만, 이와 별개로 전역 영역을 담고 있기도 합니다.

함수 선언식으로 함수를 선언하거나, var로 전역 변수를 만들면, window 객체에서도 동일한 값을 찾을 수가 있습니다.

전역 변수는 가장 바깥 스코프에 정의한 변수입니다. 따라서, 어디서든 접근이 가능합니다.

얼핏 "모든 변수를 바깥으로 빼면 스코프 걱정을 하지 않아도 되겠네?" 라는 생각이 들 수도 있습니다.
그러나, 전역 변수를 많이 만드는 것은 그다지 좋은 선택이 아닙니다.

보통 애플리케이션을 만들 때에는, 내가 직접 작성하지 않은 수많은 다른 함수와 로직이 포함됩니다. 너도나도 똑같은 이름으로 전역 변수를 선언하려고 한다면 분명 문제가 발생할 것입니다.

이를 side effect라고 합니다. 전역 변수를 최소화하는 것은 side effect를 줄이는 좋은 방법입니다. 앞서 배웠듯, var 키워드는 블록 스코프를 무시합니다. 또한 재선언을 해도 에러를 내지 않습니다. 전역 변수를 var로 선언하는 것은 브라우저의 내장 기능을 사용하지 못하게 만들 수도 있습니다. 따라서, let과 const를 주로 사용하세요.
선언 없이 변수를 할당하지 마세요. 선언 없이 변수를 할당하면, 해당 변수는 var로 선언한 전역 변수처럼 취급됩니다.

그외)
Strict Mode는 브라우저가 보다 엄격하게 작동하도록 만들어줍니다. 앞서 언급한 것처럼 "선언 없는 변수 할당"의 경우도 Strict Mode는 에러로 판단

Strict Mode를 적용하려면, js 파일 상단에 'use strict' 라고 입력하면 됩니다. (따옴표 포함)

학습 목표
스코프의 의미와 적용 범위를 이해한다.
스코프의 주요 규칙을 이해한다.
전역 스코프와 지역 스코프의 차이를 이해한다.
block scope와 function scope의 차이를 이해한다.
변수 선언 키워드(let, const, var)와 스코프와의 관계를 설명할 수 있다.
전역 객체가 무엇인지 설명할 수 있다.

클로저

"함수와 함수가 선언된 어휘적(lexical) 환경의 조합을 말한다.
이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.
"여기서 주목할 만한 키워드는 "함수가 선언"된 "어휘적(lexical) 환경"입니다.
특이하게도 자바스크립트는 함수가 호출되는 환경과 별개로 기존에 선언되어 있던 환경, 즉 어휘적 환경을 기준으로 변수를 조회하려고 합니다. 이와 같은 이유로 "외부 함수의 변수에 접근할 수 있는 내부 함수"를 클로저 함수라고 합니다.

~~학습 목표 ~~
클로저 함수의 정의와 특징에 대해서 이해할 수 있다.
클로저가 갖는 스코프 범위를 이해할 수 있다.
클로저를 이용해 유용하게 쓰이는 몇 가지 패턴을 이해할 수 있다.

함수를 리턴하는 함수가 클로저의 형태를 만듭니다.
한편, 클로저는 리턴하는 함수에 의해 스코프(변수의 접근 범위)가 구분됩니다. 클로저의 핵심은 스코프를 이용해서, 변수의 접근 범위를 닫는(closure; 폐쇄) 데에 있습니다. 따라서, 함수를 리턴하는 것만큼이나, 변수가 선언된 곳이 중요합니다.

"내부 함수는 외부 함수에 선언된 변수에 접근 가능하다"라는 점입니다. 이러한 특징이 어떤 장점을 가질까?

const adder = function(x){
	return fuction(y){
    return x+y
    }
}

외부 함수의 변수에 접근 가능한 내부 함수 : 클로저 함수
내부 함수는 외부 함수에 선언된 변수에 접근이 가능합니다.
외부 함수는 내부 함수에 선언된 변수에 접근이 불가능하다.
클로저 함수는 ‘함수를 리턴하는 함수'다.

(2)클로저의 장점? 활용?

const adder = function(x){
return fuction (y) {
return x+y;
}
}
const add5 = adder(5);
add5(7) // 12;
add5(10) // 15;

1) 데이터를 보존한다.
일반적인 함수는, 함수 실행이 끝나고 나면 함수 내부의 변수를 사용할 수 없습니다. 이와 다르게, 클로저는 외부 함수의 실행이 끝나더라도, 외부 함수 내 변수가 메모리 상에 저장됩니다. (어휘적 환경을 메모리에 저장하기 때문에 가능한 일입니다)

변수 add5 에는 클로저를 통해 리턴한 함수가 담겨 있습니다. add5 는 재미있게도, adder함수에서 인자로 넘긴 5라는 값을 x 변수에 계속 담은 채로 남아있습니다. 외부 함수의 실행이 끝났음에도 말이죠!

const tagMaker = tag => content => `<${tag}>${content}</${tag}>`

const divMaker = tagMaker('div'); 
divMaker('hello') // '<div>hello</div>'
divMaker('codestates') // '<div>codestates</div>'

const anchorMaker = tagMaker('a');
anchorMaker('go') // '<a>go</a>'
anchorMaker('urclass') '<a>urclass</a>'

2) 정보의 접근 제한
클로저 모듈 패턴

내부 함수를 여러 개 만들 수 있다.
클로저를 이용해 내부 함수를 단 하나만 리턴하는 것에 그치지 않고, 객체에 담아 여러개의 내부 함수를 리턴하도록 만든다.
const makeCounter = () => {
let value = 0;

return {
increase: () => {
value = value + 1;
}
,
decrease: () => {
value = value - 1;
}
,
getValue: () => value;
}
}

const counter1 = makeCounter();

makeCounter를 실행하여 변수에 담아봅시다.
makeCounter 함수는 increase, decrease, getValue 메서드를 포함한 객체 하나를 리턴합니다. 따라서, counter1은 객체입니다.

const makeCounter = () => {
let value = 0;

return {
increase: () => {
value = value + 1;
}
,
decrease: () => {
value = value - 1;
}
,
getValue: () => value;
}
}

const counter1 = makeCounter();
counter1 // {increase:f , decrease:f , getValut:f }
// 함수 여러개를 포함한 객체

저 위 함수에서,
value라는 변수에 값을 새롭게 할당할 수 있을까요?
nope!!! 스코프 규칙에 의해 불가능하다.
이것이 바로 정보의 접근 제한 (캡슐화) 입니다.
왜 이렇게 하는 것일까요? 만일 스코프로 value 값을 감싸지 않았더라면, value 값은 전역 변수여야만 했을 것입니다. 하지만 makeCounter라는 함수가 value 값을 보존하고 있기 때문에, 전역 변수로 따로 만들 필요가 없습니다.
전역 변수가 좋지 않은 이유는, 전역 변수는 다른 함수 혹은 로직 등에 의해 의도되지 않은 변경을 초래하기 때문입니다. 이를 side effect라고 합니다. side effect를 최소화하면, 의도되지 않은 변경을 줄일 수 있습니다. 따라서 이에 따른 오류로부터 보다 안전하게 값을 보호할 수 있습니다.
클로저를 통해 불필요한 전역 변수 사용을 줄이고, 스코프를 이용해 값을 보다 안전하게 다룰 수 있습니다.

헷갈린 퀴즈)

종합퀴즈 1 - 1번 8번 9번 10번
종합퀴즈 2 - 1번 2번

spread/rest 문법

spread 문법

주로 배열을 풀어서 인자로 전달하거나, 배열을 풀어서 각각의 요소로 넣을 때 사용한다.

function sum (x, y, z){
    return x+y+z;
}
const numbers =[1,2,3]

//sum(...numbers)
//6

rest 문법

파라미터를 배열의 형태로 받아서 사용 가능하다. 파라미터 개수가 가변적일 때 유용하다.

function sum(...theArgs){
    return theArgs.reduce((previous, current) => {
        return previous + current;
    });
}
undefined
sum(1,2,3)
6
sum(1,2,3,4)
10

배열에서 사용하기

1) 배열 합치기

let arr1 = [0,1,2]
let arr2 = [3,4,5]
arr1 = [...arr1, ...arr2];

//arr1
//[0, 1, 2, 3, 4, 5]
let parts = ['shoulders', 'knees'];
let lyrics = ['head', ...parts, 'and', 'toes'];


//lyrics
//['head', 'shoulders', 'knees', 'and', 'toes']

2) 배열 복사

let arr =[1,2,3]
let arr2=[...arr]

arr.push(4)
arr
4

3) 객체에서 사용하기

아~아마도, foo는 동일하게 있는 속성이라 속성 값을 바꿔주고, x는 없는 속성이라 추가해줬다.
obj1과 obj2의 키가 같을 경우, 값이 덮어 씌워집니다. 그렇지 않은 경우, 각각의 키와 값이 모두 그대로 전달됩니다.

let obj1 = { foo: 'bar', x: 42 };
let obj2 = { foo: 'baz', y: 13 };

let clonedObj = { ...obj1 };
let mergedObj = { ...obj1, ...obj2 };


//clonedObj
//{foo: 'bar', x: 42}
//mergedObj 
//{foo: 'baz', x: 42, y: 13}

4) 함수에서 나머지 파라미터 받아오기

function myFun(a, b, ...manyMoreArgs) {
  console.log( a);
  console.log( b);
  console.log(manyMoreArgs);
}

myFun("one", "two", "three", "four", "five", "six");
//one
//two
//['three', 'four', 'five', 'six']

알고가야할 것

reduce()

틀린 퀴즈

1)
let arr = [10, 30, 40, 20]
let value = Math.max(arr)
//value
// NaN
//Math.max를 MDN에서 검색해 보시면, 숫자인 인자들을 비교하여 가장 큰 수를 리턴하는 메서드인데, 여기서는 배열을 전달하여 실행하였기 때문에 NaN이 나옵니다.

2)
let arr = [10, 30, 40, 20]
let value = Math.max(...arr)
//여기서는 spread syntax가 사용되었습니다. spread syntax는 iterable 한 모든 것의 (대표적으로 문자열, 배열) 요소를 "펼쳐"주는 문법을 의미합니다. 즉, Math.max(10, 30, 40, 20)과 같기 때문에 정답은 B입니다.

3)

let arr = ['code', 'states']
let value = [
  ...arr,
  'pre',
  ...['course', 'student']
]
//['code', 'states', 'pre', 'course', 'student']

4)

let fruits = ['apple', 'banana', 'grape'];
let vegetables = ['tomato', 'pumpkin'];

let copiedFruits = [...fruits];
copiedFruits.push('pineapple');

let basket = [...fruits, ...vegetables]

console.log(basket)

copiedFruits는 fruits를 복사한 배열입니다. 그러나 spread 문법은 기존 배열을 변경하지 않으므로(immutable), copiedFruits를 수정하다고 해서 fruits가 함께 수정되지 않습니다.
따라서 copiedFruits에 추가 된 'pineapple'은 basket에 포함되어 있지 않습니다.
fruits와 vegetables를 spread 문법으로 할당한 보기 B가 정답입니다.

5)

function myFun(a, b, ...manyMoreArgs) {
  console.log(manyMoreArgs);
}

myFun("one", "two", "three", "four", "five", "six");

Rest문법은 파라미터의 개수가 가변적일 때 유용합니다. 문제의 코드는 a에 첫 번째 파라미터인 "one"가, b에 두 번째 파라미터인 “two”가 할당 되고, 나머지인 "three", "four", "five", "six" 는 manyMoreArgs에 배열의 형태로 할당이 됩니다. 따라서 정답은 보기D입니다.

구조분해할당

구조 분해 할당은 spread 문법을 이용하여 값을 해체한 후, 개별 값을 변수에 새로 할당하는 과정을 말합니다.
spread를 통해 값을 해체했는데 왜 나올때 []과 {}의 모습으로 나오는지 이해가 안간다.

const [a, b, ...rest] = [10, 20, 30, 40, 50];
undefined
console.log(a)
VM1591:1 10
undefined
console.log(b)
VM1644:1 20
undefined
console.log(rest)
VM1713:1 (3) [30, 40, 50]
const {a,b, ...rest} = {a:10, b:20, c:30, d:40}
undefined
console.log(a)
VM1898:1 10
undefined
console.log(b)
VM1948:1 20
undefined
console.log(rest)
VM2014:1 {c: 30, d: 40}
undefined

이건 이해가 안가는거

function whois({displayName: displayNames, fullName: {firstName: names}}){
  console.log(displayNames + " is " + names);
}

let user = {
  id: 42,
  displayName: "jdoe",
  fullName: {
      firstName: "John",
      lastName: "Doe"
  }
};

//whois(user)
 jdoe is John
profile
팀에 기여하고, 개발자 생태계에 기여하는 엔지니어로

0개의 댓글