깊은바다거북
개발 공부 기록
깊은바다거북
전체 방문자
오늘
어제
  • 분류 전체보기 (219)
    • JAVA (9)
    • JavaScript (15)
    • 스파르타코딩클럽 (11)
      • [내일배움단] 웹개발 종합반 개발일지 (5)
      • [내일배움캠프] 프로젝트와 트러블 슈팅 (6)
    • SQL | NoSQL (4)
    • CS 등등 (0)
    • TIL | WIL (173)
    • 기타 에러 해결 (3)
    • 내 살 길 궁리 (4)

인기 글

최근 글

최근 댓글

태그

  • Binary Tree(이진 트리)
  • DFS(깊이우선탐색)
  • 점화식(Recurrence Relation)
  • Til
  • BST(이진 탐색 트리)
  • Preorder Traversal(전위 순회)
  • 최대 힙(Max Heap)
  • POST / GET 요청
  • 팀 프로젝트
  • BFS(너비우선탐색)
  • 시간 복잡도
  • TypeScript
  • 최소 힙(Min Heap)
  • Backtracking(백트래킹)
  • Inorder Traversal(중위 순회)
  • 자잘한 에러 해결
  • tree
  • 자바스크립트 기초 문법
  • Linked List
  • 재귀 함수
  • 자료 구조
  • leetcode-cli
  • Leetcode
  • TIT (Today I Troubleshot)
  • 트러블 슈팅 Troubleshooting
  • 프로그래머스
  • 혼자 공부하는 자바스크립트
  • Trie
  • 01. 미니 프로젝트
  • 코딩테스트 연습문제
hELLO · Designed By 정상우.
깊은바다거북

개발 공부 기록

TIL | WIL

1/29 일 (열거형과 제네릭) TIL

2023. 1. 29. 22:25

(프로그래머스 문제 해결함)

코딩테스트 연습 - 빛의 경로 사이클
각 칸마다 S, L, 또는 R가 써져 있는 격자가 있습니다. 당신은 이 격자에서 빛을 쏘고자 합니다. 이 격자의 각 칸에는 다음과 같은 특이한 성질이 있습니다. 빛이 "S"가 써진 칸에 도달한 경우, 직진합니다. 빛이 "L"이 써진 칸에 도달한 경우, 좌회전을 합니다. 빛이 "R"이 써진 칸에 도달한 경우, 우회전을 합니다.
https://school.programmers.co.kr/learn/courses/30/lessons/86052

GRID를 만들고 좌표를 불러오는 과정에서 row, col로 나타내는 방식과 x, y 좌표로 나타내는 방식이 일정치 못한 데서 생긴 에러였다. GRID와 directionGrid를 호출하는 곳에서 [x][y]로 부르던 것을 [y][x]로 고치고, light에 들어가는 “현재 좌표”도 { x: col, y: row }로 고치니 테스트케이스가 다 통과됐다!

  • (일단) 정답 코드:
    // 이상해지기 전 (함수로 추상화하기 전) 코드 (테스트케이스:
    function solution2(gridString) {
        // 1. 2d 배열 초기화: 0으로 채워진 n x n 배열을 만든다.
        // => 3d 배열 초기화: [상, 하, 좌, 우]로 채워진 row(=GRID_HEIGHT) x col(=GRID_LENGTH) 배열을 만든다.
        const GRID_LENGTH = gridString[0].length;
        const GRID_HEIGHT = gridString.length;
        const n = 3
        // GRID:
        // ┌─────────┬─────┬─────┐
        // │ (index) │  0  │  1  │
        // ├─────────┼─────┼─────┤
        // │    0    │ 'S' │ 'L' │
        // │    1    │ 'L' │ 'R' │
        // └─────────┴─────┴─────┘
        const GRID = new Array();
        for (let i = 0; i < GRID_HEIGHT; i++) {
            GRID[i] = gridString[i].split('')
        }
        // console.log("GRID: ")
        // console.table(GRID)
    
        // directionGrid:
        // ┌─────────┬────────────────────────────----──┬─────────────────────────────----─┐
        // │ (index) │                  0               │                 1                │
        // ├─────────┼────────────────────────────----──┼─────────────────────────────----─┤
        // │    0    │  { '상': true, '하': true, ... }  │ { '상': false, '하': false, ... } │
        // │    1    │ { '상': false, '하': false, ... } │ { '상': false, '하': false, ... } │
        // └─────────┴────────────────────────────----──┴─────────────────────────────----─┘
        const directionGrid = JSON.parse(JSON.stringify(
            Array(GRID_HEIGHT).fill(Array(GRID_LENGTH).fill({
                상: false,
                하: false,
                좌: false,
                우: false
            }))
        ));
        // console.log("directionGrid: ")
        // console.table(directionGrid)
    
        const 순환경로리스트 = [];
        const 막힌경로리스트 = [];
    
    
    
        // 순환 시작
        function trackCycle(initialLight) {
            // initialLight = {
            //                  떠날때방향: "하",
            //                  현재좌표: { x: 0, y: 0 }
            //                 }
            const START_X = initialLight.현재좌표.x;
            const START_Y = initialLight.현재좌표.y;
            const 이경로발자국 = [{...initialLight.현재좌표}]
            const light = initialLight;
    
            // 2. 빛의 이동:
            function move(light) {
                let 도착좌표 = {...light.현재좌표}
                if (light.떠날때방향 == '상') {
                    if (light.현재좌표.y > 0) {
                        도착좌표.y -= 1
                    } else {
                        도착좌표.y = GRID_HEIGHT - 1;
                    }
                } else if (light.떠날때방향 == '하') {
                    if (light.현재좌표.y < GRID_HEIGHT - 1) {
                        도착좌표.y += 1
                    } else {
                        도착좌표.y = 0;
                    }
                } else if (light.떠날때방향 == '좌') {
                    if (light.현재좌표.x > 0) {
                        도착좌표.x -= 1
                    } else {
                        도착좌표.x = GRID_LENGTH - 1;
                    }
                } else {
                    if (light.현재좌표.x < GRID_LENGTH - 1) {
                        도착좌표.x += 1
                    } else {
                        도착좌표.x = 0;
                    }
                }
                // console.log("(현재좌표로 업데이트할) 도착좌표: ", 도착좌표)
    
                // const 사방위상태 = directionGrid[light.현재좌표.x][light.현재좌표.y];
    						const 사방위상태 = directionGrid[light.현재좌표.y][light.현재좌표.x];
                사방위상태[light.떠날때방향] = true;
                // light.떠나온좌표 = {...light.현재좌표};
                light.현재좌표 = {...도착좌표};
                이경로발자국.push(light.현재좌표) // 깊은 복사 처리 완료.
                // console.log("이경로발자국: ")
                // console.table(이경로발자국);
            }
    
            let loop = 1;
            // console.log(`\n--loop ${loop}--`);
            move(light)
    
            while (true) {
                loop++;
                // console.log(`\n--loop ${loop}--`);
    
                // 1. 다음 방향 탐색 (도착 직후) :
                // 1-1. light.떠날때방향 갱신:
                const 현재x = light.현재좌표.x
                const 현재y = light.현재좌표.y
                const 현재gridType = GRID[현재y][현재x];
                // console.log("현재 좌표: ", light.현재좌표)
                // console.log("현재gridType: ", 현재gridType)
    
                // 1-2. 떠나온 방향에 따른 다음 나가는 방향 { 떠나올 때 방향: 나가는 방향 }
                const gridS = { 상: "상", 하: "하", 좌: "좌", 우: "우" }
                const gridL = { 상: "좌", 하: "우", 좌: "하", 우: "상" }
                const gridR = { 상: "우", 하: "좌", 좌: "상", 우: "하" }
                if (현재gridType == 'S') {
                    light.떠날때방향 = eval('gridS.' + light.떠날때방향);
                } else if (현재gridType == 'L') {
                    light.떠날때방향 = eval('gridL.' + light.떠날때방향);
                } else {
                    light.떠날때방향 = eval('gridR.' + light.떠날때방향);
                }
                // console.log("떠날 방향: ", light.떠날때방향)
    
                // 1-3. 진짜 '떠날 수 있는지' 체크:
                // const 떠날때방향 = light.떠날때방향;
                // => 사방위상태.떠날때방향 : '떠날때방향'이라는 키로 새 속성을 만들어버림. 소용없다.
                const 사방위상태 = directionGrid[현재y][현재x];
                // console.log("떠날 수 있는지? : ", 사방위상태)
                if (사방위상태[light.떠날때방향]) { // 일단 더이상 경로를 이어가지는 못함
                    // // 만약 지금이 시작 좌표라면
                    // if (이경로발자국.length == 1) {
                    //     // 순환/막힌경로리스트에 추가하지 않고 지나감.
                    //     break
                    // }
                    // 만약 현재 좌표가 순환을 시작한 좌표라면, '순환 완성'
                    if (현재x === START_X && 현재y === START_Y) {
                        순환경로리스트.push(이경로발자국)
                        // console.log("순환 완성: 순환경로리스트 ", 순환경로리스트)
                    } else {
                        // 그게 아니라면 순환 중에 경로가 겹쳤다는 뜻. 순환이 되지 못함. 이 경로는 폐기 처분.
                        막힌경로리스트.push(이경로발자국)
                        // console.log("순환 실패: 막힌경로리스트 ", 막힌경로리스트)
                    }
                    // 이 경로 탈출
                    break;
                }
    
    
                // 2. 빛의 이동 (출발 -> 도착) :
                // 도착 좌표 계산, '현재 좌표(갱신 전)'의 떠날 방향=true 처리,
                // '현재 좌표' 갱신, '이경로'에 도착한 좌표 업데이트:
                let 도착좌표 = {...light.현재좌표}
                if (light.떠날때방향 == '상') {
                    if (light.현재좌표.y > 0) {
                        도착좌표.y -= 1
                    } else {
                        도착좌표.y = GRID_HEIGHT - 1;
                    }
                } else if (light.떠날때방향 == '하') {
                    if (light.현재좌표.y < GRID_HEIGHT - 1) {
                        도착좌표.y += 1
                    } else {
                        도착좌표.y = 0;
                    }
                } else if (light.떠날때방향 == '좌') {
                    if (light.현재좌표.x > 0) {
                        도착좌표.x -= 1
                    } else {
                        도착좌표.x = GRID_LENGTH - 1;
                    }
                } else {
                    if (light.현재좌표.x < GRID_LENGTH - 1) {
                        도착좌표.x += 1
                    } else {
                        도착좌표.x = 0;
                    }
                }
                // console.log("(현재좌표로 업데이트할) 도착좌표: ", 도착좌표)
    
                사방위상태[light.떠날때방향] = true;
                light.현재좌표 = {...도착좌표};
                이경로발자국.push(light.현재좌표) // 깊은 복사 처리 완료.
                // console.log("이경로발자국: ")
                // console.table(이경로발자국);
            }
        }
    
    
        // 다른 순환 경로 탐색
        // 각 GRID 좌표의 각 방위(directionGrid) 전부를 훑기
        for (let row = 0; row < GRID_HEIGHT; row++) {
            for (let col = 0; col < GRID_LENGTH; col++) {
                let values = Object.values(directionGrid[row][col]);
                // console.log(`${row}행 ${col}열 사방위상태: ${values}`)
                if (!values.includes(false)) {
                    // console.log('빠져나감')
                    continue;
                }
                for (let i = 0; i < Object.entries(directionGrid[row][col]).length; i++) {
                    let directions = Object.entries(directionGrid[row][col]);
                    // console.log("3중 루프 안에서 현재 사방위상태: ", directionGrid[row][col])
                    // console.log("현재 검사 방향: ", directions[i])
                    if (!directions[i][1]) { // 예를 들어 '상'방위가 false라면
                        // 새 순환 경로 추적 시작
                        // // 필요 항목: 시작 좌표, 시작 방향
                        // trackCycle(시작좌표=[row, col], 시작방향=direction[0]);
                        // 아니다 아예 light 객체를 넘기면 되겠구나
                        const newLight = {
                            떠날때방향: directions[i][0],
                            // 현재좌표: { x: row, y: col }
    												현재좌표: { x: col, y: row }
                        }
                        trackCycle(newLight)
                    }
                }
            }
        }
    
    
        // 나중에 순환경로리스트를 다 채우면
        const cycleLengths = 순환경로리스트.map(cycle => cycle.length - 1);
        // => 밟아온 총 좌표 길이에서 출발 좌표 하나를 빼야 '경로의 길이'가 됨.
        // console.log(`\n순환경로리스트: ${순환경로리스트}`)
        // console.log(`\n막힌경로리스트: ${막힌경로리스트}`)
        // console.log(`\ncycleLengths: ${cycleLengths}`)
        cycleLengths.sort((a, b) => a - b);
        return cycleLengths;
    }

이제 이것을 함수나 클래스로 분리하고, 뽑아낸 순환경로를 표시한다거나 하는 식으로 기능을 덧붙이고 싶다. 그렇지만 일단 어제 받은 조언을 반영해, 다음에 하도록 하자. 이미 과하게 시간을 썼고 할 일이 많으니.

배운 것:

타입스크립트의 열거형 Enums와 제네릭에 대하여.

(삼성 알고리즘 학습 & 풀이 사이트)

SW Expert Academy
SW 프로그래밍 역량 강화에 도움이 되는 다양한 학습 컨텐츠를 확인하세요!
https://swexpertacademy.com/main/learn/course/subjectList.do?courseId=AVuPDN86AAXw5UW6

- 문제 풀이도 강의도 C와 C++, Java, Python 밖에 제공하지 않아서 아쉽다.


제네릭

: 선언 시점이 아닌 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법

제네릭 인터페이스

예시:

interface MyInterfaceG<GenericValue> {
  value: GenericValue;
}
const stringObjectG: MyInterfaceG<string> = { value: "hello, world!" };
const numberObjectG: MyInterfaceG<number> = { value: 1234 };
const stringArrayObjectG: MyInterfaceG<Array<string>> = { // MyInterface<string[]> 기능적으로는 동일
  value: ["hello", "world!"],
};

내가 이해한 제네릭:

  1. const ~~로 변수를 생성하는 시점에 <T> 자리에 들어갈 타입이 정해진다!
    • => 이후로 타입은 그것 하나로 고정됨
  1. 타입이 “정해진다(고정된다)”의 장점 : 전달받은 타입을 검사할 수 있다, 반환할 때도 명확한 타입을 명시해 반환할 수 있다. ↔ any도 어떤 타입이든지 넣을 수 있다는 공통점이 있지만 이쪽은 아예 타입 검사를 하지 않는다는 게 다름.
  1. 함수에 인수를 넣어 호출하는 것처럼 인터페이스나 타입 옆에도 제네릭타입을 넣어서 써준다고 기억하면 되겠다.
  1. 선언 시점의 코드를 보고 T가 정확히 어떤 타입인지 알 수 있다는 소소한 장점도 있음.
  1. 제네릭을 쓸 거라고 처음 interface에 소개해주는(?) 시점에 디폴트 타입(기초값)을 넣어줄 수 있다.
    // 제네릭에 기초값 지정하기 예시 :
    interface MyInterfaceGG<GenericValue = string> {
      value: GenericValue;
    }
    const stringObjectGG: MyInterfaceGG = { value: "hello, world!" }; // ✅
    const myRandomObjectGG: MyInterfaceGG = { value: true }; // ❌ Type 'boolean' is not assignable to type 'string'.
    const myRandomObjectGG2: MyInterfaceGG<boolean> = { value: true }; // 제네릭 타입을 <boolean>이라고 명시해 줘야 에러가 안 뜬다.
  1. 제네릭으로 선언 시 한 번 만든 interface 등을 수정하지 않고 여러 타입에 재활용하여 사용할 수 있다.
    // 제네릭 없이 여러 타입이 들어올 수 있게 interface를 만들면:
    interface MyInterface {
        value: string | number | string[] // | number[] | boolean | ...
    }
    const stringObject: MyInterface = { value: "hello, world!" };
    const numberObject: MyInterface = { value: 1234 };

    ⇒ 제네릭을 사용하지 않을 땐 interface 정의 자체에 타입을 계속 추가해줘야 해 번거롭다.

막간 - MyInterface<Array<string>>과 MyInterface<string[]>

제네릭 표기 Array<string>와 배열 표기 string[]는 기능적으로는 같다.

다만 제네릭과 readonly속성은 같이 쓸 수 없다는 차이가 있다. readonly를 사용하고 싶을 땐 배열 표기 string[]와 함께 쓰든지, 유틸리티 함수 ReadonlyArray를 이용해 제네릭 표기를 하든지 해야 한다.

// 예시:
const error: readonly Array<boolean> = [false];
const okay: readonly boolean[] = [true];

// 유틸리티 함수 ReadonlyArray를 이용해 제네릭 Array와 readonly를 같이 쓸 수 있다.
const okayGeneric: ReadonlyArray<boolean> = [false];

제네릭 함수

예시:

function getData<T>(data: T): T {
    return data;
}

console.log(getData<string>("string data"));
T가 모두 string으로 잘 대체된 모습

⇒ getData(”string data”)라고만 해줘도 타입 추론이 되지만, 이 때는 “string data”라는, 내가 준 값이 캡쳐가 되어서 이 리터럴이 T의 타입으로 정해지게 된다.

console.log(getData("string data"));
제네릭 타입을 지정해주지 않고 추론하게 두면 문자열 리터럴로 추론(캡처)해버린다.

Object.keys

enum Status {
  Initiated = "Initiated",
  Pending = "Pending",
  Shipped = "Shipped",
  Delivered = "Delivered",
}

interface Order {
  buyer: string;
  orderStatus: Status;
}

// Object.keys()
// ❌ Type 'string' is not assignable to type 'Status'.
const orders: Order[] = Object.keys(Status).map((status, index) => { // 이 status는 string 타입임. 
  return {
    buyer: `buyer #${index}`,
    orderStatus: status, // 여기서 에러. 
  };
});

Object.keys()의 반환타입은 항상 string[]이고, 여기서 다시 map을 걸면 (status, index)의 status는 바로 string 타입이 된다. 그래서 enum Status 타입만 넣을 수 있는 orderStatus 자리에 string인 status를 배정해줘서 안된다고 하는 것.

해결책:

// 1번 방식 - 비추
const orders: Order[] = Object.keys(Status).map((status, index) => {
  return {
    buyer: `buyer #${index}`,
    orderStatus: status as Status,
  };
});

// 2번 방식 - .keys() 대신 .values() 사용하기
const orders: Order[] = Object.values(Status).map((status, index) => {
  return {
    buyer: `buyer #${index}`,
    orderStatus: status,
  };
});

Object.keys()의 시그니처: keys(o: object): string[];

⇒ keys<T>(o: object): string[]; 같이 되어 있지 않아서 에러가 뜸.

Object.values()의 시그니처: values<T>(o: { [s: string]: T } | ArrayLike<T>): T[];

⇒ Status가 T로 캡처되어 들어감. 리턴값이 Status[] 배열로 정해짐. values()의 인수로 넣어 줄 수 있는 것은 Status 타입을 값(들)으로 가지는 객체, Status[] 배열이다.

이쯤에서 알아보는…

열거형 Enums

: Enumerated Type.

: 기본적으로 ‘숫자’를 ‘열거했다’는 뜻이다.

: 어떤 ‘숫자’들에 특별한 의미를 담은 호칭을 붙여주고 싶다는 것이다.

: 어떤 ‘수’들을 특별한 상수로 지정하고 (문자열인)이름을 붙여주겠다는 것이다.

이것의 ‘정체’

Enum은 그 이름 자체로 타입이고, Enum.키들도 각자 타입처럼 쓰일 수 있다.

기본 형식: enum 이름 { enum키1 = enum값1, enum키2 = enum값2, … }

‘enum 값’들에는 숫자나 문자열이 올 수 있다.

‘enum 값’들의 타입은 Enum명.키가 되고, 이 안에 근본 타입인 number나 string이 들어 있다.

어떻게 만드는가

  1. 숫자형 Enum: ‘enum 값’들이 숫자인 Enum.
    enum Color {
    	Red, // = 0
    	Green, // = 1
    	Blue = 200,
    	Purple, // 201
    }
    • 디폴트 ‘enum 값’은 0부터 시작하는 ‘숫자’임.
      const yourColor = Color.Blue;
      console.log(yourColor) // 2
    • 디폴트 값 말고 명시적 선언도 가능함.
    • 리버스 매핑(Reverse mapping)이 가능함. (= ‘enum 값’으로 ‘키’를 참조하는 방법)
      // 인덱싱으로 참조
      console.log(Color[0]); // "Red"
      console.log(Color["0"]); // "Red"
      console.log(Color["Red"]); // 0
      
      // '.속성' 방식으로 참조
      console.log(Color.Red); // 0
      
      // => 이 넷 외에는 모두 에러가 난다. 
    • 리버스 매핑이 가능한 이유: 숫자형 enum의 경우 각 키-값 쌍을 뒤집은 쌍도 자동으로 같이 넣어서 저장되기 때문
      enum Color {
      	Red = 300,
      	Green,
      }
      console.log(Color)
      
      => 결과: 
      {
        '300': 'Red',
        '301': 'Green',
        Red: 300,
        Green: 301
      } 
  1. 문자형 Enum

    : ‘enum 값’이 문자열인 Enum.

    : “특별한 ‘문자’들을 별명 지어 부르겠다.”

    enum Color {
    	Red = "Red",
    	Green = "Green",
    	BLUE = "BLUE",
    	MyNickname = "Turtle"
    }
    
    console.log(Color.Red) // "Red"
    console.log(Color["Red"]) // "Red"
    • 키와 값을 대문자로 쓸지는 개인 취향이다.
    • 객체의 속성을 참조하는 것과 똑같이 .Red와 [’Red’] 방식이 모두 가능하다.
    • 문자형 Enum의 경우에는 리버스 매핑을 위한 “리버스 변환”값이 저장되지 않는다.
  1. 숫자형 + 문자형 콤보 Enum
    enum Color {
    	Red = 300,
    	Green, // = 301
      Purple = "PURPLE",
    }
    
    console.log(Color)
    
    => 
    {
      '300': 'Red',
      '301': 'Green',
      Red: 300,
      Green: 301,
      Purple: 'PURPLE'
    }
    • 뒤집어진 숫자형은 당연히 문자형 enum 값이 된다.
    • 여전히 숫자형 enum값 쌍만 리버스되고, 별다른 건 없다.

타입 타입 타입

enum Color {
	Red = 300,
	Green, // = 301
  Purple = "PURPLE",
}

const myColor = Color.Red;
console.log(myColor) // 300

const yourColor = Color.Purple;
console.log(yourColor) // "PURPLE"

myColor의 근본 타입은 number이먀, 그를 감싸고 Color.Red가 타입으로 보여진다.

yourColor의 근본 타입은 string이며, 그를 타입 Color.Purple이 또 한 번 감싸고 있다.

  • 왜 자꾸 null과 undefined를 막고 싶어 하는 걸까요
  • Color.Red는 포장 타입? 그 근본 타입은 string이라구?


Uploaded by N2T

    'TIL | WIL' 카테고리의 다른 글
    • 1/31 화 (타입스크립트 엔티티 모음과 분류) TIL, TIT
    • 1/30 월 (keyof typeof 콤보) TIL
    • 1/28 토 (2차원 배열 만들기, 반복문 순회 대상 업데이트하기) TIL, TIT
    • 1/26 목 (@types/Node가 정확히 뭔지 알게 되어 속이 시원함) TIL, TIT
    깊은바다거북
    깊은바다거북

    티스토리툴바