4. Javascript 함수와 객체

2022. 11. 4. 16:54JavaScript

목차
0. 복습
1. 함수표현식

  1.1. 함수 표현식
  1.2. 함수는 값이다
    1.2.1. callback 함수
  1.3. 즉시함수
  1.4. 화살표 함수
2. 객체

  2.1. 객체 생성 방법
    2.1.2. 객체에 키와 값을 추가 하는법
  2.2. 객체 속성정보 추출
    2.2.1. in 연산자
    2.2.2. for in 반복문
3. 스프레드 연산자(...: Spread Operator)

  3.1 스프레드(심화)

 


0. 복습

 

지난 시간에 배운 함수를 짧게 복습해보자.

 

#. 함수의 사용방법

function 예약어를 이용해 함수를 선언하며 이를 함수 선언식이라고 한다. 문법은 다음과 같다.

function 함수명(매개변수) {
	실행코드
    return
}

함수명(인자)

##. 호이스팅

 

function 에는 var같이 호이스팅이라는 기능이 존재하는데, 이 기능은 예약어와 변수를 코드 맨 처음으로 올려서 진행하는 방식이다. function은 이 기능으로 선언과 호출의 위치가 바뀌어도 실행은 되지만 좋지 못한 방법이므로 피해야한다.

 

###. 힙과 스택

 

호이스팅으로 변수명만 선언되고 호출을 했을 때, 함수가 실행되는 이유는 처음 코드를 훑을 때 힙이라는 공간에 함수를 저장하기 때문이다. 힙이라는 공간은 실행을 시키지 않되 저장하는 곳이며, 코드를 실행시켜 출력하는 공간은 스택이라고 한다. 스택에서는 한줄 한줄 코드가 순서에 따라 진행되고 처리가 늦어지는 코드는 아래로 깔리면서 순서가 꼭 코드 순서대로 출력이 되지는 않는다는 점에 유의하자.

 

####. return

return은 간단하게 함수의 코드 진행을 끝내는 명령어라 생각하면 된다.

 

 


1. 함수 표현식

 

1.1 함수 표현식

함수는 표현식이라는 방법으로도 문법을 작성이 가능하다.

기존의 함수 선언식은 아래의 코드 처럼 예약어 function로 시작하면서 작성된다.

function 함수명() {
	진행코드
}

 

반면에 함수 표현식은 아래의 코드처럼 진행된다.

const 함수명 = function() {}

코드를 한번 해석해보자면 const 함수명으로 변수를 선언하고, 그 안에 함수를 담는 형태이다.

 

이렇게 표현하게 될 경우 무슨일이 벌어질까?

  • 호이스팅이 일어나지 않는다.
  • 대입연산자 함수도 무언가의 값이다.
  • 함수명이 없는 함수다.
  • 가독성이 떨어진다.

먼저, 호이스팅이 일어나지 않는다. const로 변수 선언을 했을 때에는 function을 제일 위로 올라가서 읽는 호이스팅이 발생하지 않는다. 이로인해 코드 순서를 바꾸어서 함수호출이 먼저 진행되어도 오류가 없던 선언식과는 다르게 함수 표현식은 코드의 순서를 바꾸면 진행이 되지 않는다.

 

두 번째, 대입연산자 함수도 무언가의 값이다. 위 처럼 코드를 진행한다는 것은 변수를 선언하고 그 변수에 함수를 담는다는 뜻인데 변수에는 원래 데이터 값을 넣는다. 그러므로 함수도 데이터 값이라는 사실을 알 수 있다.

 

세 번째, const의 변수 선언명을 함수명이라고는 했지만 실제적으로 변수에 담기는 건 이름은 선언하지 않은 function의 함수식 그 자체이므로 이름이 없는 익명함수라고 할 수 있다.

 

마지막으로 가독성이 떨어진다는 단점이 생긴다. 코드를 읽을 떄, const로 변수를 선언하고 function이 나오기 떄문에 이 코드가 함수라는 것을 보기가 조금은 가독성이 떨어진다. 하지만, 이 불편한 점은 개발자 에디터로 조금이나마 해결이 가능하다.

 

 

선언식과 표현식의 차이점

 

Q. 함수 표현식과 함수 선언식은 어떤 차이가 있을까?

 

함수 표현식의 장점은 코드의 오류를 만들게 되는 호이스팅을 해결해 준다는 점이다.

반면에 앞에서 언급했듯이 가독성이 떨어지는 단점이 존재한다.

 

Q. 무엇이 더 많이 쓰이나?

 

결론부터 말하자면, 어느 표현이 더 좋다고 말할 수 없다.

포커싱을 어디에 두느냐에 따라서 차이가 있는 편이지 무엇이 더 좋다고 할 수 없다.

그러므로 두 표현 모두 잘 알고 있어야한다. 

 

1.2 함수는 값이다

 

위에서 언급했듯이 함수도 보기와는 다르게 값의 성질을 띈다. 이 사실을 한번 살펴보자.

function showMessage() {
    console.log("hello world!")
}

console.log(showMessage) // ƒ showMessage() {console.log("hello world!")}

showMessage() // hello world!

코드 진행시 "hello world!" 라는 값이 출력되도록  함수를 선언헀다.

console.log로 함수명을 찍어보면 함수 그 자체의 내용이 나오는 것을 볼 수 있다. 즉, 출력값이 나오는 것이 아니라 함수 자체가 값, 데이터라는 소리다.

 

반면, 함수호출 시 정상적으로 작동하여 "hello world!"가 출력하는 것을 확인할 수 있다.

 

함수가 값이라는 말 뜻은 함수의 출력값이 함수의 값이 아니라 함수 전체가 값이라는 뜻이 된다. 이로인해 함수는 또 다른 함수에 매개변수나 인자로 쓰일수 있다는 소리가 된다.

 

 

1.2.1 callback 함수

 

callback 함수에 대해 정의하기 전에, 함수도 값이라는 것을 이용해 함수를 함수의 인자로 넣어보자.

function hello(fn) { // 매개변수 fn
    console.log(fn)
}

function print() {
    return 10
}

두 가지 함수를 선언했다.

첫 번째의 함수는 fn을 매개변수로 갖고 진행시 console.log(fn)을 출력하는 코드이고,

두 번째 함수는 매개변수를 갖지 않고 return인 10값을 반환하는 코드다.

위의 두 함수를 이용해서, 두 가지의 출력을 실행해 보자.

hello(print()) //10
hello(print) // ƒ print(){return 10}

첫 줄의 코드는 ()로 인해 먼저 print함수의 코드 진행 후 리턴값을 매개변수에 넣어 출력하여 10이라는 값이 출력되고,

두 번째 줄의 코드는 print는 그 함수 자체를 값으로 매개변수에 넣어 함수 자체가 출력 된 모습이다.

 

이렇게 함수 자체를 매개변수로 넣는 형태를 "callback함수"라고 부른다.

 

아래의 첫번 째 코드와 두 번째 코드에는 어떤 차이가 있을까?

function hello(fn) {
    console.log(fn())
}

function print() {
    return 10
}

hello(print) // 10
function hello2(fn) {
    console.log(fn)
}

function print() {
    return 10
}

hello2(print()) // 10

첫 번째의 hello 함수는 ()안에 print 함수 자체가 있기 때문에 익명함수를 만나지 않고 힙에서 꺼내와 값을 넣어 산출하고,

두 번째의 hello2 함수는 ()안에 print()를 처리해야 하기때문에 이를 익명함수에 넘겨서 실행하여 10을 hello2에 넣어 산출한다.

 

즉, hello(print)는 익명함수를 만나지 않고, hello2(print())는 익명함수를 만난다.

(※두 호출은 익명함수를 만나고, 만나지 않아 시간 차이가 있을 거 같지만, 둘 다 한번의 실행이라는 점 때문에 처리속도에서의 큰 의미는 없다.)

 

callback 함수는 어떻게 활용해야 하는가?

다음의 코드를 한 번보자

function hello(fn) {
    let ingoo = 'javascript'
    console.log(fn(ingoo))
}

hello 함수 안에 지역변수로 ingoo가 선언되어 있다.

 

Q.나는 ingoo라는 변수의 "javascript"라는 텍스트만을 뽑아 내고 싶다면 어떻게 해야 하는가? 

 

이를 해결하기 위해서는 여러 방법이 있겠지만, callback 함수로 해결할 수도 있다.

function hello(fn) {
    let ingoo = 'javascript'
    console.log(fn(ingoo))
}

function print(name) {
    return name
}

hello(print) // javascript

위와 같이 함수를 선언하고 매개변수로 이용하면 "javascript"라는 텍스트 만을 깔끔하게 뽑아낼 수 있다.

 

즉, 밖에선 건드리기 힘든 지역변수를 callback 함수를 이용해 접근이 가능하다.

 

1.3 즉시함수

 

이번에는 간단하게만 짚고 넘어갈 함수형태이다.

console.log("hello world!") // hello world!

;(function(){
    console.log("hello world!") // hello world!
})()

위의 코드를 살펴보자면, 세미콜론";"부터 나오는 코드가 즉시함수라는 형태로 함수 선언과 동시에 코드를 해석하여 산출하는 코드이다.

 

세미콜론을 쓰는 이유는 앞 줄의 코드와 구분해 주기 위함이다.

 

맨 마지막의 () 처럼 아무것도 들어가있지 않은 괄호는 기본적으로 코드를 해석하지 않는다.

 

함수를 소개하면서 잠깐 소개한것이니 가볍게 읽고 이러한 형태가 있다는 것을 인지하고 넘어가자.

 

 

1.4 화살표 함수

 

함수의 표현방법에 대해 앞서 2가지를 배웠다.

// 함수 선언식
function sum(a, b){
    return a + b
}

// 함수 표현식
const sum2 = function (a, b) {
    return a + b
}

여기서 두 번째 방법인 함수 표현식을 조금 더 간결한 문법으로 만드는 방법이 있다. ES6에서 추가된 문법으로 

'화살표 함수(Arrow Function)'라고 한다. 화살표 함수를 사용하는 방법은 아래의 코드로 살펴보자.

//함수 표현식
const sum = function (a, b) {
    return a + b
}

// 화살표 함수
const sum = (a, b) => {
	return a + b
}

함수 표현식과 비교해서 보자면,  function이 매개변수 괄호의 뒤로 옮겨가면서 "=>"로 바뀐 것이다.

 

즉, function이 "=>"로 바뀌었다고 보면된다. 

여기서 더 줄일 수 있는데, return을 꼭 쓸 필요가 없기 때문에 return 또한 생략이 가능하다.

// 화살표 함수(return 생략)
const sum = (a, b) => a + b

return을 생략하면서 중괄호{ }도 함께 생략하여 더욱 간단한 모양으로 변했다.

 

만약, 여기서 매개변수가 하나일 경우 다음 코드와 같이 ( ) 도 생략이 가능하다.

// 화살표 함수 : 함수 표현식
const sum = (a) => {
	return a + 1
}

const sum = a => a + 1

( ) 까지 생략이 가능은 하지만 가독성 면에서 조금 헷갈릴 수 있기 때문에 선호하지 않는다.


2. 객체

 

객체(Object)란 사전적 정의로 실제 존재하는 것이라는 의미이며, JS에서는 참조형데이터에 속하는 데이터 타입이다. 참조형 데이터란 원시형 데이터와 다르게 변수에 저장된 데이터값을 가져오는 것이 아니라 변수에 저장된 데이터의 주소를 가져오는 것이다.

 

아래의 데이터를 보면 객체형 데이터 안에 배열과 객체가 있는데 이 둘은 어떻게 다르며 왜 생긴걸까?

 

예를들어 보자면,

 

노트북 이라는 상품을 구매할 때, 노트북의 정보는 이름, 가격, 회사, 스펙 등 여러가지가 있다. 여러가지 정보를 비교가 필요한데, 이 정보를 배열에 담게 된다면, 많은 상품들의 이름, 가격, 회사, 스펙 중 한가지 데이터를 담아야한다. 배열은 같은 카테고리의 정보를 순서대로 나열해 저장해 주는 데이터 이기 떄문이다.

 

이름이나 가격만을 가지고 상품을 구매하기란 쉽지 않다. 하지만, 객체는 노트북 하나 하나의 정보가 담겨 있는 것이다.

 

객체는 주로 실제로 존재하는 개체를 데이터로 표현할 떄 적합하다고 할 수 있다.

(물론, 둘 중 어느 데이터가 더 뛰어나다고는 할 수 없고 각각의 데이터에는 적합한 사용방법이 있다.)

  • 원시형
    • 문자형
    • 숫자형
    • 불리언
    • undefined
    • null ....
  • 객체형( 참조형 )
    • 배열
    • 객체 ....

 

2.1 객체 생성 방법

 

객체라는 데이터를 생성하는 방법을 알아보자.

  • new를 사용하는 생성자 문법
  • "{ }"를 생성하는 객체 리터럴 문법

주로 후자의 방법을 사용하는 편이며, 전자의 new 사용법은 다음에 알아보도록 하자.

 

아래의 코드로 객체를 생성하여 데이터를 담아 보았다.

const Product = {
    name: "Mac",
    price: 100,
}

console.log(Product.price) // 100

우선 const로 Product라는 변수를 선언하고, 중괄호 { }로 객체 데이터라는 것을 표현해준다.

그리고 나서 { } 안에 name과 price라는 키(속성)에 콜론":", "Mac"과 100이라는 데이터를 입력한 형태이다.

문법을 좀 더 살펴보자면, 중괄호 안의 키와 값의 구분은 콜론":"으로하고 속성과 속성의 구별은 쉼표","로 구분해준다.

 

2.1.1 객체 속성의 값을 가져오는 법

 

아래의 console.log 부분을 살펴보면 Product.price 를 사용했다. 이 코드의 의미는 Product안의 price라는 키를 지정해서 값을 출력하는 코드이다. Product.price 같은 형태를 "점 표기법"이라고 한다.

 

"점 표기법"의 형태는 console.log에서도 찾아볼 수 있다. console이라는 객체안의  log를 불러온다고 보면된다.

 

그리고 문법을 잘 살펴보면 마치 CSS에서의 선택자로 선택 후 키와 값을 매기는 것과 비슷하다.

 

속성명을 설정할 때, 주의할 점이 있는데 바로 특수문자(" ", "-", "_", "$" 등등)가 들어갈 때이다.

특수문자가 들어갈 떄는 표기법이 살짝 달라진다.

 

예를 들어, model name이라는 키가 있다고 생각해보자.

키를 model name이라고 넣게되면 Js는 이를 하나가 아닌 두 가지로 볼 수 있기 때문에 두 단어를 하나라고 표시해 주어야한다. 즉, "model name"이라고 넣어야 하나의 키로 오류가 나지 않게 넣을 수 있다.

 

 

객체 밖으로 나와서 console.log(Product.model name)을 읽히면, Product.model과 name 두 가지로 생각해서 오류가 나게된다. Js가 이를 구분하기가 힘들기 떄문이다. 이를 해결하기 위한 문법이 대괄호[ ] 표기법이다. 대괄호 표기법은 다음과 같다. 

 

const Product = {
    name: "Mac",
    price: 100,
    "model name": "mac book",
}

console.log(Product["mac book"])

맨 아래의 코드를 살펴보면, 점"." 대신에 대괄호[]와 그안에 쌍따옴표""를 넣어주었다. 특수문자가 들어있는 속성뿐만 아니라 name이나 price도 넣을 수 있는데 반드시 대괄호와 쌍따옴표를 [" "]형태로 바꾸어 주어야 코드가 정상작동한다.

 

2.1.2 객체에 키와 값을 추가 하는법

 

객체라는 데이터 안에 속성을 추가하는 법도 있다.

 

위에서 배운 표기법들을 활용하면, 값을 뽑아내는 것이 아닌 추가가 가능하다.

const Product = {
    name: "Mac",
    price: 100
}

Product.color = "white"
console.log(Product) // {name: 'Mac', price: 100, color: 'white'}

위와 같이 속성이 추가 된 것을 확인이 가능하다.

 

여기서 의문이 든다. 분명 예약어 const를 이용하면 값을 재할당이 불가능한데 어떻게 가능한가?

 

객체는 const로 선언했기에 재할당이 불가능한 것은 맞지만, 재할당이 불가는 한 것은 참조값이 불가능한 것이지 참조하고 있는 원래의 데이터는 재할당이 가능하다. 


<그림 1>

 


왼쪽의 Product라는 객체 안의 A라는 주소는 바꿀 수 없다, 하지만 오른쪽의 참조하고 있는 원래의 데이터는


<그림 2>


<그림 2>와 같이 새로운 속성을 추가하는 게 가능하다. 

 

2.2 객체 속성 정보 추출

 

위에서 소개한 점 표기법과 대괄호 표기법을 이용해서 객체 안 속성의 키를 입력해 값을 뽑고, 키를 생성했다.

 

이가 가능한것은 키의 이름을 알기 떄문이다. 그렇다면 키를 모를 때는 키를 어떻게 알 수 있으며, 값은 어떻게 알 수 있을까?

 

2.2.1 in 연산자

 

객체는 안에 없는 키를 보려고 하면, 에러가 뜨지 않고 undefined로 반환한다. 이 특징을 이용하면 객체안에 값이 있는 지 연산자"==="를 통해서 알 수 있다.

 

이 외에 in 연산자를 사용하여 알 수 있는데, in 연산자의 사용법은 [ "속성 이름" in 객체 ]로 사용하고 있다면 boolean타입으로 true나 false를 출력한다.

 

2.2.2 for in 반복문

 

반복문 for와 연산자 in을 사용하면 객체의 모든 키를 추출할 수 있다.

const Product = {
    name: "Mac",
    price: 100,
}

for (let key in Product) {
    console.log(key)
    console.log(Product[key])
}

/*name
"Mac"
price
100*/

이 때 key라는 변수명은 다른 변수명으로 지어도 정상적으로 실행된다. for 와 in을 이용한 특별한 연산이다.

많은 개발자들이 조금 더 복잡한 코드로 객체를 추출하다가 효율을 높이기 위해 새롭게 약속한 문법이다.

하지만 for in문 마저도 조금 길어 다음에 나옴 연산자를 새롭게 만들어냈다.


3. 스프레드 연산자(...: Spread Operator)

스프레드 연산자는 for in 문의 과정을 "..." 이라는 새로운 연산자로 정의한 것이므로 기능에는 차이가 없다.

 

스프레드 연산자를 이용해 조금 더 심화된 객체 개념을 이해해보자. 다음의 코드를 살펴보자.

console.log({}==={}) // false

위의 코드가 false가 나오는 이유는 {}와 {}가 모양은 같지만, 저장된 주소의 위치가 다르기 때문이다.


<그림 3>


A와 B라고 적혀있진 않지만 편의상 적어놓고 보면, A라는 주소를 할당한 { }와 B라는 주소를 할당한 { }와는 차이가 있는 것을 알 수 있다.

 

3.1 스프레드(심화) 

아래의 코드를 한번 살펴보면,

const Product = {
    name: "Mac",
    price: 100,
}

const clone = {...Product}

console.log(Product) // {name: 'Mac', price: 100}
console.log(clone) // {name: 'Mac', price: 100}
console.log(clone===Product) // false

위에서 본 코드의 결과처럼 내용은 그대로 출력이 되지만 다른 객체임을 출력하고 있다.

 

원본을 A 클론을 B라고 한다면, A가 참조하고있는 데이터를 바꾸었을 때, 클론 B의 데이터에도 영향을 줄까?

const Product = {
    name: "Mac",
    price: 100,
}

const clone = {... Product}

console.log(Product)//{name: 'Mac', price: 100}

Product.name = "ACC" // 원본 참조데이터 변경

console.log(clone)//{name: 'Mac', price: 100}
console.log(Product)//{name: 'ACC', price: 100}

원본의 데이터를 바꾸게 되면 당연하게 객체 Product의 속성의 값 부분이 바뀌지만, 복사본은 그전 Product를 복사한 시점에 머물러 있음을 확인할 수 있다. 즉, 복사는 그 시점의 데이터 상태를 복사하는 개념이라고 생각하자.

 

const Product = {
    name: "Mac",
    price: 100,
    color: {
        red: 255,
        green: 255,
        blue: 255,
    }
}

const clone = {...Product}

console.log(Product) // name: 'Mac', price: 100, color: {red: 254, green: 255, blue: 255}
console.log(clone) // name: 'Mac', price: 100, color: {red: 254, green: 255, blue: 255}
console.log(clone===Product)// false

const dopple = {...Product}
console.log(dopple) // name: 'Mac', price: 100, color: {red: 254, green: 255, blue: 255}
console.log(dopple.color===Product.color) //true

const dopple2 = {...Product, color:{...Product.color}}
console.log(dopple2) // name: 'Mac', price: 100, color: {red: 255, green: 255, blue: 255}
console.log(dopple2===Product) // false
console.log(dopple2.color===Product.color) // false

Product.color.red = 254

console.log(dopple.color===Product.color)// true
console.log(dopple2.color===Product.color)// false

console.log(dopple) // ...color.red:254, 
console.log(dopple2) // ...color.red:255,

보기 힘들겠지만, 위의 코드를 순서대로 잘펴보자면, Product 객체 안에 color라는 객체를 담은 모습이다.

 

이를 clone으로 스프레드 연산자를 이용해 복사 했을 때 당연히 "===" 비교시 false값이 출력된다.

 

여기서 궁금한 점은 원본 Product를 복사하는데, dopple2와 같이 스프레드 연산자가 두번 들어 간다면, 한번 쓴 경우와 어떤 차이가 있는지이다.

 

간단하게 그림으로 설명해보자면, 


<그림 4>

dopple이 Product를 복사할 때의 시점이며, 얇은 화살표를 참조, 굵은 화살표를 스프레드 값을 dopple에 대입한다고 생각하자. 여기서 볼 점은 color라는 키는 하나의 객체로 다른 어딘가에서 속성들을 참조받고 있다.

 

<그림 5>

<그림 5>는 복사 직후의 시점이다. 객체 dopple은 스프레드 연산자를 한번 사용하여 color객체의 주소값 자체를 가져왔기 때문에, color가 참조하고 있는 데이터를 똑같은 형태로 참조하고 있는 모습이다.

 

이후 스프레드를 두 번 사용한 객체를 생성하는 코드의 복사 시점을 보자.

<그림 6>

<그림 6>에서 보면 굵은 화살표가 두 번. 즉, 스프레드 연산이 두 번임을 볼 수 있다.스프레드를 두 번 사용하면,  원본 객체에 담긴 객체, color의 주소 값이 아닌 직점 color의 속성이 있는 곳으로가서 속성의 키와 값을 가져온다.

<그림 7>

dopple2의 복사가 끝난 직후의 시점이며, 어느데이터도 참조하지 않고 있다.

 

이 때, color가 참조하고 있는 속성의 값 red의 255값을 254로 바꾸게 되었을 때, 세 객체의 red 값은 어떻게 출력이 되는지 보자.

<그림 8>

<그림 8>과 같이 Product의 red값과 dopple의 red값에 영향을 동시에 준다.

 

위에 적은 코드를 다시 한번 가져와 살펴보자.

const Product = {
    name: "Mac",
    price: 100,
    color: {
        red: 255,
        green: 255,
        blue: 255,
    }
}

const dopple = {...Product}
console.log(dopple) // name: 'Mac', price: 100, color: {red: 254, green: 255, blue: 255}
console.log(dopple.color===Product.color) //true

const dopple2 = {...Product, color:{...Product.color}}
console.log(dopple2) // name: 'Mac', price: 100, color: {red: 255, green: 255, blue: 255}
console.log(dopple2===Product) // false
console.log(dopple2.color===Product.color) // false

Product.color.red = 254

console.log(dopple.color===Product.color)// true
console.log(dopple2.color===Product.color)// false

console.log(dopple) // ...color.red:254, 
console.log(dopple2) // ...color.red:255,

결과적으로 객체안에 객체 값은 스프레드를 두 번 사용했을 경우에 값만 가져온다는 걸 알 수 있다.

 

추가로 출력된 코드를 보면 의문인 점이 있는데 아래에서 color:red의 값이 바뀌었는데 위에 있는 코드도 바뀌어 있는 걸 확인할 수 있는데, 이는 참조해서 보여주는 데이터이기 떄문에 나중에 코드가 바뀌더라도 참조하고 있기 떄문에 위에서도 바뀌어 출력된다.


 

'JavaScript' 카테고리의 다른 글

6. Javascript 객체 지향의 개념, 유용한 메서드  (0) 2022.11.08
5. Javascript 메서드, 생성자 함수  (0) 2022.11.07
3. Javascript 함수의 개념  (0) 2022.11.02
2. Javascript 기초(2)  (0) 2022.11.02
1. Javascript 기초(1)  (1) 2022.11.02