connecting dots

Beginner 5회차 1 (7/30) | 팩토리 함수, 고차 함수, 함수 커링, 불변성과 가변성★, 타입 추론(Inference), 타입별칭(Type Aliases)와 interface 본문

Live Class/Beginner

Beginner 5회차 1 (7/30) | 팩토리 함수, 고차 함수, 함수 커링, 불변성과 가변성★, 타입 추론(Inference), 타입별칭(Type Aliases)와 interface

dearsuhyun 2024. 7. 30. 16:05

팩토리함수(Factory Function)

객체 데이터(참조형 데이터)를 반환하는 함수

function getUser() {
  return {
    name: "John",
    age: 30,
  };
}
const user = getUser();
console.log(user); // { name: 'John', age: 30 }
console.log(getUser()); // { name: 'John', age: 30 }

console.log(user.name); // John
console.log(getUser().name); // John

// 모든 함수는 호출되면 return으로 반환하는 값을 가지고 있다.

 

고차함수

함수를 인수로 전달하거나(콜백함수) 반환값으로 사용하는 함수

function useUser() {
  // 팩토리함수이자 고차함수
  return function () { //return에서는 익명함수를 많이 씀
    return {
      name: "John",
      age: 30,
    };
  };
}

const getUser = useUser(); // useUser를 호출하여 함수를 반환받음
const creatUser = getUser(); // // 반환된 함수를 호출하여 객체를 얻음

useUser()().name; // 함수커링, John
getUser().name; // John

 

함수 커링

함수가 함수를 반환한다 !

커링: 여러 개의 인수를 받는 함수에서 인수를 한 번에 하나씩 받는 함수로 나누어서 순차적으로 호출하는 기법

함수 커링(Currying)은 여러 개의 인자를 받는 함수를, 인자를 하나씩 받아서 새로운 함수를 반환하는 방식으로 변환하는 기술입니다. 즉, 하나의 함수가 여러 인자를 한 번에 받는 대신, 하나의 인자를 받고 또 다른 함수를 반환하는 과정을 반복합니다. 이 방식은 함수형 프로그래밍에서 자주 사용됩니다.

원래함수
function add(x, y) {
  return x + y;
}

add(2, 3); // 5​


커링된 함수

function add(x) {
  return function(y) {
    return x + y;
  };
}

const add2 = add(2); // add2는 y를 받아서 2 + y를 반환하는 함수
console.log(add2(3)); // 5
console.log(add(2)(3)); // 5​

주어진 예제
function useUser() {
  // 팩토리 함수이자 고차 함수
  return function() { // return에서는 익명 함수를 많이 씀
    return {
      name: "John",
      age: 30,
    };
  };
}

const getUser = useUser(); // useUser를 호출하여 함수를 반환받음
const creatUser = getUser(); // 반환된 함수를 호출하여 객체를 얻음

console.log(useUser()().name); // John
console.log(getUser().name); // John


• useUser()().name는 useUser 함수를 호출하여 반환된 함수를 다시 호출하고, 최종적으로 반환된 객체의 name 속성에 접근합니다. 결과는 "John"입니다.

• getUser().name도 같은 방식으로 반환된 객체의 name 속성에 접근하여 "John"을 얻습니다.

 

 

불변성과 가변성★★★★★

const users = [{ name: 'N'}, { name:'Y' }] 
// 초기화 코드, 배열 데이터가 메모리상에 만들어짐. 
// users = 메모리의 주소로 참조하는 개념. 실제 데이터는 M1에 있음
// users는 M1을 바라보고 있음

const copyUsers = users // 할당 연산자의 역할 = 메모리 주소를 전달하는 역할. copyUsers도 M1을 바라봄
copyUsers[0].name = 'X'
console.log(users[0].name) // 'X'


코드 설명

const users = [{ name: 'N'}, { name:'Y' }]
M1 M2 M3 M4
[{ name: 'N'}, { name:'Y' }]       

 

--> 하나의 메모리에는 하나의 데이터만 들어갈 수 있음

M1 M2 M3 M4
[M2, M3]  { name: 'N'} { name:'Y' }  

M1 M2 M3 M4
[M2, M3]  { name: M4} { name: M5 } 'N'
M5 M6 M7 M8
'Y'      

 

--> users는 M1을 참조 --> M2 --> M4, M3 --> M5

 

const copyUsers = users // 할당 연산자의 역할 = 메모리 주소를 전달하는 역할. copyUsers도 M1을 바라봄
copyUsers[0].name = 'X'
M1 M2 M3 M4
[M2, M3] { name: M4}
--> { name: M6}
{ name: M5 }  'N'
M5 M6 M7 M8
'Y' 'X'    

 

'=' 할당연산자의 역할은 메모리 주소를 전달하는 역할 ! 따라서 copyUsers도 M1을 바라봄

copyUsers[0] = M2

copyUsers[0].name 속성을 'X'로 변경하면

'X'는 원시형 데이터로써 현재 메모리 안에 없었기 때문에 M6에 새롭게 저장

M2에서 {name: M4}는 copyUsers[0].name= 'X' 코드로 인해 {name: M6}로 바뀜

 

console.log(users[0].name) // 'X'

 

copyUsers[0]name 속성을 ‘X’로 변경하면, users 배열의 첫 번째 요소도 ‘X’로 변경됨

copyUsersusers가 동일한 객체를 참조하고 있기 때문에 !

 

 

 

1. 얕은 복사

객체나 배열을 복사할 때, 최상위 레벨의 데이터(배열의 각 요소(객체)의 참조)만 복사하고 그 안에 포함된 객체나 배열은 원본 객체와 동일한 참조를 가지게 되는 복사 방식

즉, 객체 내의 객체는 복사되지 않고, 원본과 복사본이 동일한 내부 객체를 참조하게 됨 --> 복사본을 고쳤더니 원본도 고쳐짐

M1 M2 M3 M4
[M2, M3] { name: M7} { name: M5 }  'N'
M5 M6 M7 M8
'Y' [M2, M3] 'X'  

 

const users = [{ name: 'N'}, { name:'Y' }] 
const copyUsers = [...users] // 전개 연산자. 얕은 복사 [{ name: 'N'}, { name:'Y' }]
copyUsers[0].name = 'X'
// copyUsers의 첫 번째 객체는 users 배열의 첫 번째 객체와 동일한 참조를 가집니다. 
// 따라서 copyUsers[0]와 users[0]는 같은 객체를 참조합니다.
console.log(users[0].name) // 'X'

 

비주얼 예제

// 초기 상태
const users = [ { name: 'N' }, { name: 'Y' } ];
const copyUsers = [ { name: 'N' }, { name: 'Y' } ]; // 얕은 복사

// 복사 후 상태 (여전히 같은 객체를 참조)
users[0]    copyUsers[0]
  |           |
  V           V
  { name: 'N' }   <-- 동일한 객체
  { name: 'Y' }   <-- 동일한 객체

// copyUsers[0].name = 'X' 수행 후
users[0]    copyUsers[0]
  |           |
  V           V
  { name: 'X' }   <-- 변경된 동일한 객체
  { name: 'Y' }   <-- 동일한 객체

// console.log(users[0].name) 결과
'X' <-- 변경된 동일한 객체의 name 속성

 

2. 깊은 복사

객체의 모든 계층을 복사하여, 원본 객체와 복사본 객체가 서로 독립적인 상태를 유지함

M1 M2 M3 M4
[M2, M3] { name: M4} { name: M5 }  'N'
원시 데이터(객체 데이터를 복사해도 여기 그냥 있음)
M5 M6 M7 M8
'Y' [M7, M8] { name: M4} { name: M5 }
M9      
'X'      

 

import {cloneDeep} from 'lodach' 
// lodash 라이브러리에서 cloneDeep 함수를 가져옵니다. 
// cloneDeep 함수는 깊은 복사를 수행하는 유틸리티 함수입니다.

const users = [{ name: 'N'}, { name:'Y' }] 

const copyUsers = cloneDeep(users) // 깊은 복사
// copyUsers 배열은 users 배열의 모든 요소와 동일한 값을 가지지만, 각각 독립된 객체를 포함

copyUsers[0].name = 'X'
// copyUsers 배열의 첫 번째 객체의 name 속성을 'X'로 변경
// 이 변경은 원본 배열 users에 영향을 미치지 않음

console.log(users[0].name) // M4인 'N'
console.log(copyUsers[0].name) //'X'

 

비주얼 예제

// 초기 상태
const users = [ { name: 'N' }, { name: 'Y' } ];
const copyUsers = cloneDeep(users); // 깊은 복사

// 깊은 복사 후 상태 (독립된 객체를 가짐)
users[0]         copyUsers[0]
  |                 |
  V                 V
{ name: 'N' }   { name: 'N' }   <-- 복사된 객체 (독립적임)
{ name: 'Y' }   { name: 'Y' }   <-- 복사된 객체 (독립적임)

// copyUsers[0].name = 'X' 수행 후
users[0]         copyUsers[0]
  |                 |
  V                 V
{ name: 'N' }   { name: 'X' }   <-- 변경된 객체 (독립적임)
{ name: 'Y' }   { name: 'Y' }   <-- 복사된 객체 (독립적임)

// console.log(users[0].name) 결과
'N' <-- 원본 객체는 변경되지 않음

// console.log(copyUsers[0].name) 결과
'X' <-- 복사본 객체는 변경됨

 

 

cf. 재귀함수: 나를 다시 호출하는 것

// 재귀적(Cursive) = 종료조건 전까지는 나를 무한호출하는 것
// 종료조건이 필요함

function a() {
  a()
}
a()

 

 

배열, 객체데이터 = 다른 메모리 주소를 바라보고 있다 (참조하고 있다) --> 참조형 데이터
참조하고 있는 값이 바뀔 수 있음(메모리 안에서 값이 변할 수 있음) --> '가변성 있다 !!'
문자 데이터 = 생 데이터, 원시형 데이터 --> 메모리에 한번 할당되면 안 바뀜 --> '불변성'

'불변성을 유지하면서 개발하세요' =  한번 메모리에 할당되었지만 바뀔 수있는 애들도 안 바뀌도록 조심스럽게 개발하는 것

 

 

** 예제

M1 M2 M3 M4
1 2 null  

 

let a = 1 // M1을 바라보게 됨 (데이터는 M1에 있고 a는 M1의 주소를 참조)
let b = a // 메모리 주소 할당(M1을 바라보게 됨)

b = 2 // M2을 바라보게 됨

b = 1 // 메모리에 새로 안 만들어짐 (M1에 있으므로). M1을 다시 바라보게 됨

b = null // 계속 M3에 있음

 

 

응용 - undefined와 null

  const [users, setUsers] = useState<User[]>([])
  //초기값 빈 배열. user타입의 객체데이터가 들어가야 한다(타이핑을 해줌 --> 명시적으로)
  // 명시적 = 내가 직접, null
  // 암시적 = 자동, undefined

 

함수 a 선언:
함수 a는 매개변수 b를 받아서 b + 1을 계산합니다. 하지만 이 계산 결과는 어디에도 저장되지 않으며, 반환값(return)이 명시적으로 지정되지 않았습니다.

함수 호출:
console.log(a());는 함수 a를 호출하고, 그 결과를 콘솔에 출력합니다. a 함수는 반환값이 없으므로 undefined를 반환합니다.

--> 함수에서 명시적으로 return을 사용하지 않으면, 함수는 암시적으로 undefined를 반환합니다. 이는 JavaScript의 기본 동작입니다.

 

변수 b 선언:
변수를 선언했지만 값을 할당하지 않으면, 변수의 초기값은 자동으로 undefined가 됩니다.

변수 출력:
console.log(b);는 변수 b의 값을 콘솔에 출력합니다. 변수 b는 초기화되지 않았기 때문에 undefined입니다.

 

타입추론(Inference)

TypeScript에서 변수, 매개변수, 함수 반환값 등의 타입을 명시적으로 지정하지 않아도, 컴파일러가 코드의 문맥을 통해 자동으로 타입을 추론하는 기능

타입추론이 가능한 곳에서는 타입을 안 써도 됨 하지만 그렇지 않은 경우에는 명확하게 타입을 선언해줘야 함

  const [users, setUsers] = useState([]) // never !
  
  const [message, setMessage] = useState('') // 추론 가능
  const [loading, setLoading] = useState(true) // 추론 가능

 

 

타입별칭(Type Aliases)와 interface

1. Type

타입 별칭(Type Aliases): type은 새로운 이름을 기존 타입에 부여할 때 사용됩니다. 이는 단순한 객체 형태뿐만 아니라, 기본 타입, 유니언 타입, 튜플 등 다양한 타입에 별칭을 줄 수 있습니다.

유연성: type을 사용하면 객체 타입 외에도 유니언 타입, 교차 타입, 튜플 등을 정의할 수 있습니다.

확장 불가: type은 확장이 불가능합니다. 한 번 정의된 타입은 변경할 수 없습니다.

 

2. interface

확장성(Extensibility): interface는 확장이 가능합니다. 상속을 통해 다른 인터페이스를 확장하거나, 동일한 이름의 인터페이스를 여러 번 선언하여 속성을 추가할 수 있습니다.

 구조적 타이핑(Structural Typing): interface는 객체의 구조를 설명하는 데 사용되며, 주로 클래스와 객체 간의 계약을 정의할 때 사용됩니다.

상속(Inheritance): interfaceextends 키워드를 사용하여 다른 인터페이스를 상속할 수 있습니다.

export interface Todo {
  id: string
  order: number
  title: string
  done: boolean
  createdAt: string
  updatedAt: string
}

interface ExtendedTodo extends Todo {
  good: boolean
}

 

cf. Declaration Merging

동일한 이름의 인터페이스를 여러 번 선언하여 속성을 추가하는 것

TS에서 Declaration Merging은 같은 이름의 인터페이스를 여러 번 선언하면, 각각의 선언이 하나의 인터페이스로 합쳐지는 기능

이는 특히 라이브러리의 타입 정의를 확장하거나, 기존 타입 정의를 보완할 때 유용함

 

특징)

기존의 인터페이스를 변경하지 않고도, 새로운 속성을 추가할 수 있음

여러 선언을 하나로 병합하여, 코드베이스의 다양한 부분에서 인터페이스를 확장할 수 있음

병합된 인터페이스는 모든 속성을 포함하므로, 타입 안전성을 유지할 수 있음

 

주의)

동일한 이름의 속성을 다른 타입으로 선언하면 오류가 발생함 --> 속성 이름과 타입이 일치해야 함

여러 선언이 병합될 때 타입 일관성을 유지하는 것이 중요함

 

라이브러리 타입 정의 확장 예제

// 기존의 라이브러리 타입 정의
interface LibConfig {
  host: string;
  port: number;
}

// 사용자가 추가 속성을 선언
interface LibConfig {
  protocol: "http" | "https";
  timeout: number;
}

// 최종 병합된 인터페이스
const config: LibConfig = {
  host: "localhost",
  port: 8080,
  protocol: "https",
  timeout: 1000
};

 

3. 예제

 

interface의 경우 객체를 지정할 때 사용하고(객체 데이터는 interface 사용을 더 권장) 확장성이 있어 추후 확장을 하기 위해 사용하는데

확장을 하지 않을 것이라면 type으로 만들어도 상관은 없음 !

타입은 객체 뿐만 아니라 데이터 타입을 지정할때 사용하며, type의 별칭을 만드는 역할을 함

 


* 목록 = list (n개)
항목 = item (1개)

* 주입할 데이터들 = props
주입(injection) = 밖에 있는 데이터(prop)를 컴포넌트에 넣어주는 것

 

https://www.heropy.dev/p/N6phSt

 

JS 함수 핵심 패턴

자바스크립트 함수에 대한 기본적인 사용 방법부터 관련 용어, 고급 기법 등의 관련된 다양한 함수 패턴과 여러 개념을 살펴봅니다.

www.heropy.dev

 

반응형