TIL
06.01 TIL (이력서 세션 및 커리어 데이)
2026. 6. 1. 21:17

일정표

시간 할 일 비고
08:00~09:00 코드카타, Atanny  
09:00~10:00 C++ 공부 배치고사 준비를 위한 전체 개념 복습
10:00~12:00 C++ 배치고사  
12:00~13:00 C++ 문제 리뷰  
14:00~18:00 이력서 세션, 커리어 데이  
19:00~21:00 개인 공부(프로그래머스, 언리얼 엔진 에디터)  

(20:20) 개인 공부의 경우 오늘은 이력서 작성과 자기소개서 내용을 어느 정도 채워놓고 싶어서 9시까지 자기소개서 및 채용 공고를 탐색하고, 저녁 9시에 운동 후 진행하기 함.

 

(22:00~23:30) 일정 변경으로 하지 못했던 개인 공부를 1시간 30분 정도 진행(프로그래머스 기초문제 + 정렬 Topic 3문제).

(23:00 프로그래머스 기초문제 풀이 + 추가 풀이 공부 과정에서 개념 학습을 더 진행해서 시간이 별로 남지 않아 정렬 Topic 은 내일로 연기)


*오늘의 코드카타*

 

정수를 저장한 배열, arrr에서 가장 작은 수를 제거한 배열을 리턴하는 함수, solution을 완성해주세요. 단, 리턴하려는 배열이 빈 배열인 경우엔 배열에 -1을 채워 리턴하세요. 예를 들어 arr이 [4,3,2,1]인 경우는 [4,3,2]를 리턴 하고, [10]이면 [-1]을 리턴합니다.

#include <string>
#include <vector>

using namespace std;

bool comp(int a, int b) {
    return a < b;
}

vector<int> solution(vector<int> arr) {
    vector<int> answer;
    int len = arr.size();
    
    if (arr.size() <= 1) {
        return {-1};
    }
    
    int min_idx = 0;
    for (int i = 0; i < len; i++) {
        if (comp(arr[i], arr[min_idx])) {
            min_idx = i;
        }
    }
    arr.erase(arr.begin() + min_idx);
    
    answer = arr;
    return answer;
}

for문과 erase를 활용해서 구현했다. bool comp 함수를 선언해서 최솟값을 찾기 위해 만약 min_idx(초기 인덱스 0) 보다 작은 값이 나오면 min_idx를 i값으로 덮어씌웠고, 최종적으로는 arr.erase(arr.begin() + min_idx);로 가장 작은 값을 지워 배열을 반환시켰다.

 

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> solution(vector<int> arr) {
    vector<int> answer;
    int len = arr.size();
    
    if (len <= 1) {
        return {-1};
    }
    
    auto min_it = min_element(arr.begin(), arr.end());
    
    arr.erase(min_it);
    
    answer = arr;
    return answer;
}

아니면 <algorithm>헤더에 내장되어있는 min_element 메서드를 이용해서 최솟값을 가진 인덱스를 반환할 수 있다. 그렇게 반환 받은 인덱스 번호를 min_it 반환자에 저장하고(순환하면서 찾아야 하므로 auto min_it), arr.erase(min_it); 으로 가장 작은 원소 값을 지운 후 반환한다.


*그 외*

[문제 풀이]

1. 배열 요소 합 구하기

더보기

문제

정수 벡터 v의 모든 원소 합을 반환하는 long long sum(const vector<int>& v)를 작성하시오. 비어 있으면 0.

#include <iostream>
#include <vector>
using namespace std;

long long sum(const vector<int>& v) {
    long long temp = 0;

    for (int num : v) {
        temp += num;
    }
    return temp;
}

int main() {
    vector<int> v{ 3, 6, 7, 9 };
    cout << sum(v) << "\n"; // 25
}

반복문을 이용해서 풀 수 있다. for (int i = 0; ~~)꼴의 반복문을 for (int num : v) ~ 이런 식으로 쓸 수 있다는 것도 잘 숙지해놓자.

 

#include <iostream>
#include <vector>
#include <numeric>

using namespace std;

long long sum(const vector<int>& v) {
    return std::accumulate(v.begin(), v.end(), 0LL);
}

int main() {
    vector<int> v{ 3, 6, 7, 9 };
    cout << sum(v) << "\n"; // 25
}

numeric 헤더에 포함되어있는 accumulate 메서드를 사용해서 배열의 합을 구할 수도 있다. 이때, 세번째 인자에 0LL을 써서 나타내는데 0LL은 long long 타입의 0을 의미하는 것으로 long long 타입으로 자동으로 형변환을 해준다.

 

2. 윤년 판정하기

더보기

문제

연도 y가 윤년이면 true, 아니면 false를 반환하는 bool is_leap(int y)를 구현하시오. (규칙: 400의 배수이거나 (4의 배수이면서 100의 배수는 아님))

조건

윤년은 보통 4년마다 한 번씩 돌아옵니다. (즉, 연도가 4의 배수이면 윤년)

100의 배수인 해는 윤년에서 제외합니다.

400의 배수인 해는 다시 윤년으로 인정합니다.

#include <iostream>
using namespace std;

bool is_leap(int y) {
    if ((y % 400 == 0) || (y % 4 == 0) && (y % 100 != 0)) {
        return true;
    }
    return false;
}

int main() {
    cout << boolalpha << is_leap(2000) << " " << is_leap(1900) << " " << is_leap(2024) << "\n"; // true false true
}

주어진 조건이 명확하므로 이렇게 if문을 단순하게 작성하는 연습을 하자. true, false를 반환해야 하는 케이스도 명확하고, ||, && 논리 연산자를 적극적으로 사용하면 단순하게 작성할 수 있다.

#include <iostream>
using namespace std;

bool is_leap(int y) {
    return (y % 400 == 0) || (y % 4 == 0) && (y % 100 != 0);
}

int main() {
    cout << boolalpha << is_leap(2000) << " " << is_leap(1900) << " " << is_leap(2024) << "\n"; // true false true
}

정답 자체가 true 아니면 false 구조이므로, 위와 같이 return 안에서 한줄로 작성할 수 있다.

 

3. 직사각형 클래스 구현하기

더보기

문제

class Rect를 구현하시오.

private: int width, height

생성자 (width, height)

int area() const

void setWidth(int), void setHeight(int) : 0 미만이면 0 저장

#include <iostream>
using namespace std;

class Rect {
private:    
    int width;
    int height;

public:
    Rect(int w, int h) {
        setWidth(w);
        setHeight(h);
    }

    int area() const {
        return width * height;
    }

    void setWidth(int w) {
        if (w < 0) {
            width = 0;
        }
        else {
            width = w;
        }
    }

    void setHeight(int h) {
        if (h < 0) {
            height = 0;
        }
        else {
            height = h;
        }
    }
};

int main() {
    Rect r(3, 4);
    cout << r.area() << "\n"; // 12
    r.setWidth(-5); r.setHeight(2);
    cout << r.area() << "\n"; // 0
}

private에서는 클래스의 속성인 width와 height를 선언하고 캡슐화. 그리고 public으로는 생성자와 메서드를 열어놓았다.

생성자 안에서는 setWidth(w), setHeight(h)를 선언하고 setWidth, setHeight 메서드를 조건문과 함께 추가한다.

 

4. 간단한 은행 계좌 클래스

더보기

문제

은행 계좌를 관리하는 BankAccount 클래스의 생성자 로직과 printInfo 함수를 제외한 멤버 함수 로직을 전부 구현해주세요. 단, 잔액은 음수가 될 수 없으며 출금 시 잔액이 부족하면 false 반환을 해야 합니다.

#include <iostream>
#include <string>
using namespace std;

class BankAccount {
private:
    string owner;
    double balance;

public:
    BankAccount(const string& name, double initial) {
        owner = name;
        if (initial < 0) {
            balance = 0;
        }
        else {
            balance = initial;
        }
    }

    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    bool withdraw(double amount) {
        if (amount < 0 || amount > balance) {
            return false;
        }
        balance -= amount;
        return true;
    }

    double getBalance() const {
        return balance;
    }

    void printInfo() const {
        cout << "Owner: " << owner << ", Balance: " << balance << " won\n";
    }
};

int main() {
    BankAccount account("Alice", 1000);
    account.printInfo(); // Owner: Alice, Balance: 1000 won

    account.deposit(500);
    account.printInfo(); // Owner: Alice, Balance: 1500 won

    if (account.withdraw(2000)) {
        cout << "Withdraw OK!\n"; // 이건 출력 안됨
    }
    else {
        cout << "Withdraw FAILED!\n"; // 이것이 출력됨
    }

    if (account.withdraw(1000)) {
        cout << "Withdraw OK!\n"; // 이것이 출력됨
    }

    account.printInfo(); // Owner: Alice, Balance: 500 won
}

출금 로직이 작성할 때 좀 어색한데, bool이니까 무조건 true 아니면 false가 반환되어야 한다는 사실을 잊지말자. 그래서 출금이 안전하게 완료 되면 return true;로 마무리 지어줘야한다.

 

5. 가상함수 오버라이드

더보기

문제

class Animal { virtual void speak(); }에서 파생 클래스 Dog, Cat를 만들고 각각 "Woof", "Meow"를 출력하도록 오버라이드하시오. Animal* 배열로 다형성 출력

#include <iostream>
#include <vector>
using namespace std;

class Animal {
public:
    virtual ~Animal() {}
    virtual void speak() {
        cout << "???" << "\n";
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "Woof\n";
    }
};

class Cat : public Animal {
public:
    void speak() override {
        cout << "Meow\n";
    }
};

int main() {
    vector<Animal*> zoo{ new Dog, new Cat, new Animal };
    for (auto p : zoo) p->speak(); // Woof / Meow / ???
    for (auto p : zoo) delete p;
}

부모 클래스인 Animal에서 함수를 가상함수로 선언했으므로, 컴파일러는 포인터의 타입(Animal*) 이 아닌 실제로 메모리에 생성된 객체의 타입(Dog나 Cat)을 실행 시간에 추적하여 알맞은 함수를 호출하게 된다. 그래서, 자식 클래스에서 부모의 가상 함수를 재정의하는 override를 붙여준다. 

가상 소멸자 같은 경우에는, 다형성을 사용할 때 부모 클래스의 소멸자를 가상으로 만드는 것이 필수적이므로 추가한다.

 

6. 제네릭 함수 템플릿 구현하기

더보기

문제

두 개의 값을 인자로 받아 더 큰 값을 반환하는 제네릭 함수 get_max를 작성하시오. int, double, string 등 크기 비교가 가능한 모든 타입에 대해 동작해야 합니다.

#include <iostream>
#include <string>
using namespace std;

template <typename T>
T get_max(const T& a, const T& b) {
    return (a > b) ? a : b;
}

int main() {
    cout << get_max(10, 20) << "\n";            // 20
    cout << get_max(3.14, 2.71) << "\n";        // 3.14
    string s1 = "Apple", s2 = "Banana";
    cout << get_max(s1, s2) << "\n";            // Banana

    return 0;
}

템플릿은 이론 공부 이후에 거의 써본적이 없어서 아직도 생소한 개념이어서 접근이 어려웠다.

템플릿을 선언하고, 크기 비교가 가능한 모든 타입에 대해 동작해야 하므로 범용적인 get_max 함수를 구현한다. 이때, const T& 형식으로 상수 참조 전달을 한다. 참조자(&)를 사용하여 복사를 방지하고, 함수 내부에서 값을 수정할 수 없도록 const를 붙인다. 이후 삼항연산자로 간단하게 조건식을 작성하고 값을 반환한다.

 

7. 깊은 복사 생성자 및 소멸자 구현하기

더보기

문제

IntPtr 클래스는 정수형 포인터 ptr을 멤버로 가집니다. 객체가 복사될 때, 포인터 주소만 복사되는 것이 아니라 실제 메모리 공간을 새로 할당하고 값을 복사(깊은 복사)하도록 복사 생성자를 작성하고 객체 소멸 시 불리는 소멸자도 작성해주세요.

#include <iostream>
using namespace std;

class IntPtr {
    int* ptr;
public:
    IntPtr(int val) {
        ptr = new int(val);
    }

    ~IntPtr() {
        delete ptr;
    }

    IntPtr(const IntPtr& other) {
        ptr = new int(*other.ptr);
    }

    int getValue() const {
        return *ptr;
    }
};

int main() {
    IntPtr p1(10);
    IntPtr p2 = p1; // 복사 생성자 호출

    cout << p1.getValue() << "\n"; // 10
    cout << p2.getValue() << "\n"; // 10
}

동적 할당된 메모리를 해제하여 메모리 누수를 방지하고, 깊은 복사를 구현하기 위해서 새로운 메모리 공간을 동적으로 할당한 뒤에 원본 객체(other)가 가리키는 값을 찾아와서 새로 할당한 공간에 복사하는 복사 생성자를 구현한다. 

복사 생성자( IntPtr(const Intptr& other) : IntPtr p2 = p1; 처럼 기존 객체를 이용해서 새로운 객체를 초기화할 때 호출된다. 만약 이 복사 생성자를 직접 작성하지 않으면 컴파일러가 주소값만 그대로 복사하는 얕은 복사를 수행하게 된다. 따라서, new int() 를 호출하여 완전히 새로운 메모리 공간을 만들고, 그곳에 *other.ptr(원본 객체가 가리키는 실제 정수 값)을 집어넣는 깊은 복사를 구현한다. 이렇게 한 뒤, p1과 p2는 서로 다른 메모리 공간을 가지게 되어 안전하게 소멸을 할 수 있다.

 

8. 벡터에서 중복 제거하기

더보기

문제

정수 벡터에서 중복된 값을 제거하고 고유한 값만 남긴 벡터를 반환하는 함수인 removeDuplicates 로직을 작성해주세요. 단, 원래 순서는 유지되어야 합니다.

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> removeDuplicates(const vector<int>& v) {
    vector<int> result;
    
    for (int num : v) {
        if (find(result.begin(), result.end(), num) == result.end()) {
            result.push_back(num);
        }
    }
    return result;
}

int main() {
    vector<int> nums = { 1, 3, 2, 3, 1, 4, 2, 5 };
    vector<int> result = removeDuplicates(nums);

    cout << "Result: ";
    for (int n : result) {
        cout << n << " ";
    }
    cout << "\n"; // 1 3 2 4 5
}

algorithm 헤더의 find를 사용해서 result 배열에 같은 값이 존재하는지 찾고, 같은 값이 없으면 push_back으로  result 배열에 값을 밀어넣는 방식으로 구현할 수 있다.

 

#include <iostream>
#include <vector>
#include <unordered_set>

using namespace std;

vector<int> removeDuplicates(const vector<int>& v) {
    vector<int> result;
    unordered_set<int> memo;

    for (int num : v) {
        if (memo.find(num) == memo.end()) {
            result.push_back(num);
            memo.insert(num);
        }
    }
    return result;
}

int main() {
    vector<int> nums = { 1, 3, 2, 3, 1, 4, 2, 5 };
    vector<int> result = removeDuplicates(nums);

    cout << "Result: ";
    for (int n : result) {
        cout << n << " ";
    }
    cout << "\n"; // 1 3 2 4 5
}

좀 더 성능을 향상시키고 싶다면 unordered_set 헤더를 사용해서 구현할 수 있다. unordered_set은 특정 데이터가 존재하는지 탐색할 때 매우 뛰어난 성능을 보인다. 그래서, unordered_set<int> memo; 이렇게 한번 본 숫자를 기억해두는 메모장을 만들고, memo.find(num)으로 메모장에서 num을 찾고, 만약 찾지 못한다면 컨테이너의 끝을 의미하는 end()를 반환하게 한다. 따라서, 새로운 숫자를 발견하면 result 벡터에 순서대로 차곡차곡 담게 된다.

 

9. 두 문자열이 애너그램인지 판별하기

더보기

문제

두 문자열이 애너그램 관계인지 판별하는 함수를 작성해주세요. 만약 애너그램이 아니라면, 한 문자열의 문자 몇 개를 바꿔야 다른 문자열과 애너그램 관계가 될 수 있을까요? 그 최소 횟수를 구해야 합니다.

  • 애너그램이면 0을 반환합니다.
  • 애너그램이 아니면 최소 변경 횟수를 반환합니다.
  • 변경: 한 문자를 다른 문자로 바꾸는 것을 의미합니다. (문자 1개 변경 = 1회)
  • 두 문자열의 길이가 다르면 -1 을 반환합니다.
  • 자세한 내용은 아래 코드의 main 함수 출력 예시를 참고하세요.

애너그램이란?

두 영어 단어가 철자의 순서를 뒤바꾸어 같아질 수 있을 때, 그러한 두 단어를 서로 애너그램 관계에 있다고 한다. 예를 들면 occurs 라는 영어 단어와 succor 는 서로 애너그램 관계에 있는데, occurs의 각 문자들의 순서를 잘 바꾸면 succor이 되기 때문이다. (출처: 백준)

#include <iostream>
#include <string>
#include <vector>


using namespace std;

int getMinChangesToAnagram(const string& s1, const string& s2) {
    if (s1.length() != s2.length()) {
        return -1;
    }

    int counts[256] = { 0 };
    for (char c : s1) {
        counts[(unsigned char)c]++;
    }

    int changes = 0;

    for (char c : s2) {
        if (counts[(unsigned char)c] > 0) {
            counts[(unsigned char)c]--;
        }
        else {
            changes++;
        }
    }
    return changes;


}

int main() {
    cout << "listen -> silent: " << getMinChangesToAnagram("listen", "silent") << "\n"; // 0
    cout << "abc -> abd: " << getMinChangesToAnagram("abc", "abd") << "\n"; // 1
    cout << "aabbcc -> aabdde: " << getMinChangesToAnagram("aabbcc", "aabdde") << "\n"; // 3
    cout << "abc -> xyz: " << getMinChangesToAnagram("abc", "xyz") << "\n"; // 3
    cout << "ab -> abc: " << getMinChangesToAnagram("ab", "abc") << "\n"; // -1
    cout << "test -> test: " << getMinChangesToAnagram("test", "test") << "\n"; // 0
}

크기가 256(ASCII 문자 개수)배열을 만들어서 모든 문자에 안전하게 대응할 수 있도록 배열을 초기화시켜놓는다.

이 아이디어가 중요한데, s1 배열을 각각 개수를 세고, s2랑 비교해봤을 때, counts 배열이 0보다 큰 값을 가지고 있으면 중복되는 문자라는 뜻이므로 개수를 1개 줄인다. 그리고, s1에 없거나 이미 다 소진된 문자라면 변경해야 할 문자로 카운트 한다.(changes++;)

사실 좀 더 고민해보아야 하겠지만, 이런 생각을 해내기가 지금 단계에서는 좀 어려운 것 같다. 

 

 

10. Map 활용(데이터 자동 정렬, 가격 자동 갱신)

더보기

문제

상점에 등록될 아이템은 특정 규칙에 따라 자동으로 정렬되여야하며, 동일한 아이템이 들어올 경우 가격 갱신 로직이 실행되어야 합니다.

요구사항

  • 자료구조 정의: Item 구조체를 Key로, int(가격)를 Value로 하는 std::map을 사용하십시오.
  • 정렬 기준
    • 1순위: 공격력(attackPower) 내림차순 (높은 것이 앞)
    • 2순위: 공격력이 같다면 희귀도(rarity) 오름차순 (낮은 것이 앞)
    • 3순위: 위 두 조건이 모두 같다면 이름(name) 오름차순
  • 데이터 처리 (tryInsert 함수):
    • 필터링: 공격력이 50 미만인 아이템은 상점에 등록하지 않고 무시합니다.
    • 중복/갱신: 이미 상점에 있는 아이템(이름, 공격력, 희귀도가 모두 일치)이 다시 들어올 경우, 새로운 가격기존 가격보다 높을 때만 업데이트합니다.
#include <iostream>
#include <string>
#include <map>
#include <vector>

using namespace std;

struct Item
{
    string name;
    int attackPower;
    int rarity;


    bool operator<(const Item& other) const
    {
        if (attackPower != other.attackPower) {
            return attackPower > other.attackPower;
        }
        if (rarity != other.rarity) {
            return rarity < other.rarity;
        }
        return name < other.name;
    }
};

void tryInsert(map<Item, int>& shop, const Item& item, int price)
{
    if (item.attackPower < 50) {
        return;
    }

    auto it = shop.find(item);
    if (it != shop.end()) {
        if (price > it->second) {
            it->second = price;
        }
    }
    else {
        shop[item] = price;
    }
}

int main()
{
    map<Item, int> shop;

    tryInsert(shop, { "Excalibur",      100,    1 }, 5000);
    tryInsert(shop, { "Dragon Sword",   100,    2 }, 4500);
    tryInsert(shop, { "Wooden Sword",   20,     5 }, 100);
    tryInsert(shop, { "Excalibur",      100,    1 }, 5200);
    tryInsert(shop, { "Excalibur",      100,    1 }, 4800);

    cout << "--- 상점 아이템 목록 (정렬 및 갱신 결과) ---\n";
    for (const auto& kv : shop)
    {
        const Item& item = kv.first;
        int price = kv.second;

        cout << item.name << " [ATK: " << item.attackPower
            << ", Rarity: " << item.rarity << "] : Price(" << price << ")\n";
    }

    return 0;
}

지금 단계에서는 난이도가 상당히 어려운 문제 같았다. 일단 접근이 좀 어려웠다. 많이 공부해야 할 것 같다.

내림차순 정렬, 오름차순 정렬을 1순위, 2순위, 3순위로 나누어서 정렬을 하도록 구현하는 방법을 연습할 수 있었다. 우선순위 검사의 경우 각 기준마다 값이 같지 않을 때만 즉시 return으로 대소 관계를 판별하고, 값이 같다면 그 아래 줄로 넘어가서 다음 순위의 기준을 검사하도록 체인 형태로 작성한다.

중복 여부를 확인할 때 단순히 shop[item]을 사용하면 해당 아이템이 없을 경우 맵에 기본값(0)으로 쓰레기 데이터를 자동 생성해버리는 부작용이 있다. 따라서, find()메서드를 사용해서 탐색을 하고(it != shop.end())나서 가격에 따라서 갱신하는 것이 안전하다.

it->second 가 뭔지 몰랐다. 이를 살펴보자면,

맵의 이터레이터(it)가 가리키는 객체는 Item 구조체가 map 형태로 들어가 있으므로 pair<const Item, int> 형태라는 것을 알 수 있다. 여기에서 it->first는 Key값인 Item 구조체를, it->second는 Value값인 price를 나타내게 된다.

 

 

11. Vector 만들어보기

더보기

문제

아래는 다양한 데이터 타입을 저장할 수 있는 동적 배열 클래스 MyVector의 일부 코드입니다. 주석으로 표시된 [빈칸] 부분을 채워 프로그램을 완성하세요.

#include <iostream>
#include <string>

template <typename T>
class MyVector
{
private:
    T* Data;      
    int Size;     
    int Capacity; 

public:
 
    MyVector(int InCapacity = 2)
    {
        Size = 0;
        Capacity = InCapacity;
        Data = new T[Capacity];
    }

    ~MyVector()
    {
        if (Data) delete[] Data;
    }

    void Add(const T& InData)
    {
        if (Size == Capacity) {

            int NewCapacity = Capacity * 2;

            T* NewData = new T[NewCapacity];

            for (int i = 0; i < Size; ++i) {
                NewData[i] = Data[i];
            }

            delete[] Data;
            
            Data = NewData;
            Capacity = NewCapacity;

        }

        Data[Size] = InData;
        Size++;

    }

    // [문제 3] operator[] 오버로딩 (5점)
    // 인덱스를 통해 데이터를 참조형으로 반환하세요.
    T& operator[](int Index)
    {
        return Data[Index];
    }

    int GetSize() const { return Size; }
    int GetCapacity() const { return Capacity; }
};

int main()
{
    // 테스트: 초기 용량은 2지만, 3개를 넣어서 재할당이 일어나는지 확인합니다.
    MyVector<int> Vec;
    Vec.Add(10);
    Vec.Add(20);

    std::cout << "Before Resize - Size: " << Vec.GetSize() << ", Capacity: " << Vec.GetCapacity() << std::endl;

    Vec.Add(30); // 여기서 재할당 로직이 실행되어야 함

    std::cout << "After Resize - Size: " << Vec.GetSize() << ", Capacity: " << Vec.GetCapacity() << std::endl;
    std::cout << "Data: " << Vec[0] << ", " << Vec[1] << ", " << Vec[2] << std::endl;

    return 0;
}

생성자 : 아직 들어온 데이터가 없으므로 Size = 0, Capacity는 인자로 받은 값으로 설정한다. 그리고 new T[Capacity]를 통해 힙 영역에 메모리 공간을 확보하여 포인터 Data에 연결한다.

 

C++ vector 라이브러리가 동작하는 원리를 코드로 재현한 것이라고 생각하면 되는데, Size == Capacity라면 T* NewData = new T[NewCapacity]로 기존 공간보다 더 큰 메모리 공간을 확보하고, for 루프로 기존 데이터를 복사해서 가지고 온다. 이후 delete를 사용해서 기존 Data는 메모리 할당을 해제한다.

마지막으로 Data = NewData로 덮어 씌운 뒤에, 원래 넣으려고 했던 InData를 넣는다.

 

operator[] 오버로딩의 경우 배열처럼 Vec[0] 형태로 접근하기 위한 문법이다. 반환형이 T& 이기 때문에 값을 읽어오는 것 뿐만 아니라 값을 수정하는 것도 가능하다.

 

난이도가 좀 어려워서, 일단 어느정도 사용방법과 코드 작성법을 암기해두고 접근 자체가 어려웠던 만큼 암기한 것을 꺼낼 수 있도록 준비해야 할 것 같다.


[프로그래머스]

 

1. 모음 제거

더보기

영어에선 a, e, i, o, u 다섯 가지 알파벳을 모음으로 분류합니다. 문자열 my_string이 매개변수로 주어질 때 모음을 제거한 문자열을 return하도록 solution 함수를 완성해주세요.

#include <string>
#include <vector>

using namespace std;

string solution(string my_string) {
    string answer = "";
    
    for (char c : my_string) {
        if (c != 'a' && c != 'e' && c != 'i' && c!= 'o' && c !='u') {
        answer += c; 
        }
    } 
    return answer;
}

(1) "모음 아닌 문자만 가져와서 answer에 붙이기"

 

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

string solution(string my_string) {
    string answer = "";
    my_string.erase(remove_if(my_string.begin(), my_string.end(), [](char c) {
        return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
    }), my_string.end());
    
    answer = my_string;
    
    return answer;
}

(2) "Erase-Remove" 사용하기

가장 특이한 것은 [](char c) { return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'; }

이름이 없는 함수, 즉 "람다식"이다.

remove_if 는 문자열의 처음부터 끝까지 순회하면서 람다식에 넣어보고, 람다식이 true를 반환하는 문자는 무시하고 false를 반환하는 문자들을 문자열의 앞쪽으로 당겨온 후, 이 경우 자음만 남기고 그 끝부분의 반환자를 반환한다.

 

 

2. 문자열 정렬하기

더보기

문자열 my_string이 매개변수로 주어질 때, my_string 안에 있는 숫자만 골라 오름차순 정렬한 리스트를 return 하도록 solution 함수를 작성해보세요.

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> solution(string my_string) {
    vector<int> answer;
    
    for (char c : my_string) {
        if (c >= '0' && c <= '9') {
            answer.push_back(c - '0');
        }
    }
    sort(answer.begin(), answer.end());
    
    return answer;
}

(1) char 문자형은 아스키코드여서 컴퓨터는 숫자값을 각각 할당해서 저장한다. 따라서, '0' <= c <= '9' 의 조건식을 작성해도 숫자(문자 형태의)값을 잘 뽑아낼 수 있고, 이를 sort(begin(),end())를 사용해서 오름차순 정렬할 수 있다.

 

#include <string>
#include <vector>
#include <algorithm>
#include <cctype>

using namespace std;

vector<int> solution(string my_string) {
    vector<int> answer;
    
    for (char c : my_string) {
        if (isdigit(c)) {
            answer.push_back(c - '0');
        }
    }
    sort(answer.begin(), answer.end());
    
    return answer;
}

(2) 굳이 C++ 라이브러리에 내장된 헤더를 사용하고 싶다면, <cctype> 헤더를 포함시킨 뒤에 isdigit() 함수를 사용하는 방법이 있다. 부울 함수로서 해당 문자가 숫자라면 true를 반환한다. 따라서, 조건식에 '0' <= c <= '9' 대신에 isdigit(c) 를 써넣기만 해도된다. 조건식을 만족한다면 똑같이 c - '0' 으로 해당 문자 형태의 숫자에 맞는 고유 숫자값을 만들어서 answer 배열에 집어넣는다.

 

#include <string>
#include <vector>
#include <set>
#include <cctype>

using namespace std;

vector<int> solution(string my_string) {
    multiset<int> numbers;
    
    for (char c : my_string) {
        if (isdigit(c)) {
            numbers.insert(c - '0');
        }
    }
    
    vector<int> answer(numbers.begin(), numbers.end());
    
    return answer;
    
}

(3) C++ 컨테이너에 내장된 오름차순 기능 활용하기(multiset + insert)

set을 활용하여 문제를 풀어보는 것도 생각해보았다. 주의할 점은 set의 경우 오름차순으로 자동 정렬되기는 하나 중복된 값을 지워버린다는 것이다. 따라서 중복된 값도 보존하고 오름차순으로 정렬되는 multiset을 사용해야한다. 특히, multiset 내부는 보통 균형 이진 탐색 트리 구조로 구현되어 있어 값을 insert 할 때마다 기존 데이터들과 크기를 비교하여 올바른 위치에 삽입되므로 별도로 sort함수를 부를 필요가 없다.

이러면 저장된 multiset의 begin(), end()까지 호출하기만 하면 answer 배열이 잘 완성된다.

실행속도만 보면 (1)의 vector에 다 넣고 sort하는 방식이 보통 더 빠르다. 다만 multiset을 사용하면 가독성 측면에서도 좋고 데이터가 실시간으로 많이 들어오는 상황이라면 좋은 선택이 될 수 있을 것이다.

 

 

3. 숨어있는 숫자의 덧셈(1)

더보기

문자열 my_string이 매개변수로 주어집니다. my_string안의 모든 자연수들의 합을 return하도록 solution 함수를 완성해주세요.

2번 문제의 응용. 3가지 방법으로 직접 풀어보았다.

#include <string>
#include <vector>

using namespace std;

int solution(string my_string) {
    int answer = 0;
    
    for (char c : my_string) {
        if (c >= '0' && c <= '9') {
            answer += (c - '0');
        }
    }
    return answer;
}

(1) if (c >= '0' && c <= '9')  조건문 사용해서 '0' 빼고 answer에 더하기

 

#include <string>
#include <vector>
#include <cctype>

using namespace std;

int solution(string my_string) {
    int answer = 0;
    
    for (char c : my_string) {
        if (isdigit(c)) {
            answer += (c - '0');
        }
    }
    return answer;
}

(2) C++ 라이브러리의 <cctype> 헤더의 isdigit() 활용하기

 

#include <string>
#include <vector>
#include <cctype>
#include <numeric>


using namespace std;

int solution(string my_string) {
    int answer = 0;
    vector<int> numbers;
    
    for (char c : my_string) {
        if (isdigit(c)) {
            numbers.push_back(c - '0');
        }
    }
    
    answer = accumulate(numbers.begin(), numbers.end(), 0);
    
    return answer;
}

(3) <numeric> 헤더의 accumulate() 함수 이용하기

문자열의 '문자형 숫자'만 뽑아내서 numbers vector 배열에 담고, 숫자로 된 배열의 원소들을 accumulate로 합을 구하면 된다.

 

4. 소인수분해

더보기

소인수분해란 어떤 수를 소수들의 곱으로 표현하는 것입니다. 예를 들어 12를 소인수 분해하면 2 * 2 * 3 으로 나타낼 수 있습니다. 따라서 12의 소인수는 2와 3입니다. 자연수 n이 매개변수로 주어질 때 n의 소인수를 오름차순으로 담은 배열을 return하도록 solution 함수를 완성해주세요.

#include <string>
#include <vector>

using namespace std;

vector<int> solution(int n) {
    vector<int> answer;
    
    for (int i = 2; i <= n; i++) {
        if (n % i == 0) {
            answer.push_back(i);
            while(n % i == 0) {
                n /= i;
            }   
        }
    }
    return answer;
}

핵심 아이디어는 2부터 나누기 시작해서, 나머지가 0이라면 나머지가 0이 아닐때까지 계속 다시 나눠보는 것이다. 그래야 합성수로 나누는 것을 막을 수 있다.

 

 

5. 접미사 배열

더보기

어떤 문자열에 대해서 접미사는 특정 인덱스부터 시작하는 문자열을 의미합니다. 예를 들어, "banana"의 모든 접미사는 "banana", "anana", "nana", "ana", "na", "a"입니다.
문자열 my_string이 매개변수로 주어질 때, my_string의 모든 접미사를 사전순으로 정렬한 문자열 배열을 return 하는 solution 함수를 작성해 주세요.

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<string> solution(string my_string) {
    vector<string> answer;
    
    for (int i = 0; i < my_string.length(); i++) {
        answer.push_back(my_string.substr(i));
    }
    sort(answer.begin(), answer.end());    
    return answer;
}

(1) algorithm 헤더의 sort와 substr을 사용해서 작성할 수 있다. 이 때 문자열 string은 += 로 배열에 추가할 수 없다는 사실을 잊지 말고, my_string.substr(i) 처럼 i번째 인덱스 부터 끝까지 잘라내는 부분 문자열을 만들 수 있다. 어차피 끝까지 자를 거면 굳이 end()를 쓰지 말자.

 

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<string> solution(string my_string) {
    int n = my_string.length();
   
    vector<int> suffix_indices(n);
    for (int i = 0; i < n; i++) {
        suffix_indices[i] = i;
    }
    
    
    sort(suffix_indices.begin(), suffix_indices.end(), [&my_string](int a, int b) {
      
        return my_string.compare(a, string::npos, my_string, b, string::npos) < 0;
    });
  
    vector<string> answer;
    answer.reserve(n);
    
    for (int i = 0; i < n; i++) {
        answer.push_back(my_string.substr(suffix_indices[i]));
    }
    
    return answer;
}

(2) 조금 어려운 방식도 공부해보자. 난이도 높은 알고리즘 문제에서는 공간 효율과 실행 속도의 밸런스가 가장 뛰어난 접미사 배열 구조를 많이 사용한다. (참고로 suffix는 접미사라는 뜻이고 indices는 index의 복수형이다)

그러니까, 실제 접미사 문자열을 메모리에 전부 잘라 저장하는 대신, 접미사가 시작되는 인덱스만 배열에 저장해두고 이 정수들을 사전순 기준에 따라 정렬하는 것이다. 이렇게 하면 문자열을 복제하지 않고 원본 문자열 1개와 정수 배열 1개만 유지하므로 공간 복잡도가 O(N)으로 획기적으로 줄어든다.

시간 복잡도의 경우 O(N log N)까지 단축할 수 있다.

 

먼저 vector<int> suffix_indices(n)을 선언한다. n은 기존에 입력받은 문자열의 길이이다. 이 정수 인덱스만 담은 벡터를 사용하므로 정렬과정의 추가 메모리가 단축되는 것이다.

이제 람다함수와 compare 메서드를 사용하여 정렬 기준을 정의한다. sort 함수 내부에 [&my_string](int a, int b) {...} 형태의 람다식을 사용한다.

이때 비교를 위해 my_string.substr(a) < my_string.substr(b)를 쓰게 되면 비교할때마다 문자열이 새로 생성되어 메모리와 시간이 낭비된다. 따라서 C++ 내장 함수인 compare을 사용해서 새로운 문자열을 복사하지 않고 원본 문자열의 메모리 주소에서 직접 문자들을 비교하는 것이다.

 

람다 문법을 좀 더 알아보자면,

[ ] : 캡처 블록이라고 부른다. 함수 내부에서 만든 람다 함수는 원래 바깥쪽에 있는 변수를 마음대로 가져다 쓸 수 없다. 그런데 [my_string]이라고 쓰면 바깥에 있는 my_string 변수를 이 함수로 가져와 쓸 수 있게 된다. 이 경우에는 새로운 문자열을 복사하지 않고 원본 문자열의 메모리 주소에서 직접 비교하기 위해 [&my_string] 이라고 참조 전달을 한 것이다. 이후, (int a, int b) 와 같이 매개변수를 선언해준다.

 

그럼 string::npos는 무엇일까?

한마디로 "끝까지" 또는 "지정한 범위가 없음"을 나타내는 C++ 문자열의 특별한 기호이다. npos가 애초에 no position의 줄임말이기 때문.

얼마나 잘라낼 것인지 적는 곳에 그냥 귀찮으니까 string::npos를 넣는다고 보면 된다. 그러면 C++에서 알아서 문자열 맨 끝까지 전부 다 처리하라는 뜻으로 알아듣게 된다. 

 

결과적으로 compare 메서드는 다음과 같이 사용되게 된다.

my_string.compare(a, string::npos, my_string, b, string::npos) < 0 

원본 문자열의 a번째 인덱스부터 시작해서 끝까지 자르고, my_string과 비교한다. 이번 케이스에서는 같은 문자열 내에서 비교하므로 my_string 을 다시 적어준 것이다. 다음으로는 원본 문자열의 b번째 인덱스부터 끝까지를 잘라서 비교한다. 한마디로 my_string의 a부터 끝까지의 내용과 b부터 끝까지의 내용을 비교하겠다는 뜻이다. 그리고, C++의 compare 함수는 true, false 가 아니라 숫자를 반환하는데, 두 문자열이 완전히 같으면 == 0, 앞의 문자열이 사전순으로 더 작을 때 < 0, 앞의 문자열이 사전순으로 더 클 때 > 0 을 반환한다. 따라서 우리가 원하는 오름차순으로 만들기 위해서는 compare의 결과값이 음수가 나와야 하므로 < 0 을 붙이게 된 것이다. 이렇게 하면 한문장으로 이렇게 정리된다.

"a에서 시작하는 접미사//가 b에서 시작하는 접미사//보다 사전순으로 앞에 오면// true를 반환//해라"

 

(참고)

만약 string::npos를 사용하지 않는다면?

int len_a = my_string.length() - a;
int len_b = my_string.length() - b;

return my_string.compare(a, len_a, my_string, b, len_b) < 0;

이렇게 작성할 수 있다. 사실 속도나 메모리 차이는 거의 없으나 코드 가독성 측면에서 npos를 사용하는 것에 비해 뒤떨어진다(코드가 길어짐).

 

 

6. 접미사인지 확인하기

더보기

어떤 문자열에 대해서 접미사는 특정 인덱스부터 시작하는 문자열을 의미합니다. 예를 들어, "banana"의 모든 접미사는 "banana", "anana", "nana", "ana", "na", "a"입니다.
문자열 my_string과 is_suffix가 주어질 때, is_suffix가 my_string의 접미사라면 1을, 아니면 0을 return 하는 solution 함수를 작성해 주세요.

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

int solution(string my_string, string is_suffix) {
    int answer = 0;
    vector<string> suffix_string;
    for (int i = 0; i < my_string.length(); i++) {
        suffix_string.push_back(my_string.substr(i));
    }
    
    for (int j = 0; j < suffix_string.size(); j++) {
        if (is_suffix == suffix_string[j]) {
            answer = 1;
            break;
        }
        else {
            answer = 0;
        }
    }
    return answer;
}

앞 문제에서 풀었던 내용을 응용하면 이렇게 작성할 수 있었다.

다만 조금 더 개선할 방법이 있었다. 접미사인지 확인하기 위해서 굳이 모든 접미사를 매번 새로운 문자열로 생성하여 벡터에 저장할 필요가 없다는 것이다. my_string의 맨 뒤에서부터 is_suffix의 길이만큼만 잘라서 한번만 비교하면 훨씬 빠르고 메모리도 적게 사용할 수 있다.

 

#include <string>

using namespace std;

int solution(string my_string, string is_suffix) {
    int answer = 0;
    
    //is_suffix 문자열이 더 길면 애초에 접미사가 아님.
    if (my_string.length() < is_suffix.length()) {
        return 0;
    }
    
    //is_suffix 문자열의 길이만큼 my_string 문자열을 뒤에서부터 잘라낼 시작 인덱스
    int start_idx = my_string.length() - is_suffix.length();
    
    //잘라냈는데, is_suffix와 같으면 접미사가 맞음.
    if (my_string.substr(start_idx) == is_suffix) {
        return 1;
    }
    return 0;
}

필요한 헤더도 적고 코드 길이도 적고 메모리도 적고 좋은 구조를 완성할 수 있다.


[Unreal C++]

 

언리얼 C++의 특징

언리얼 엔진(Unreal Engine)에서 사용하는 C++는 모던 C++의 특징을 바탕으로, 엔진 자체의 강력한 매크로와 가비지 컬렉션(Garbage Collection) 시스템이 결합된 형태이다. 흔히 '언리얼 C++'라고 부를 정도로 독자적인 생태계를 구축하고 있다.

따라서 표준 C++ 지식도 중요하지만, 언리얼 엔진의 구조에 맞는 C++ 활용법을 익히는 것이 중요하겠다. 본격적인 개발을 위해 반드시 알아야 할 C++ 필수 지식들은 다음과 같다.

 

1. 객체 지향 프로그래밍 (OOP)

언리얼 엔진은 철저하게 객체 지향으로 설계되어 있으므로, 클래스의 상속, 다형성, 캡슐화에 대한 깊은 이해가 필요하다.

 

1) 상속 (Inheritance): 

UObject, AActor, APawn, ACharacter 등 언리얼의 핵심 클래스 계층 구조를 이해하고 상황에 맞게 상속받아 사용해야 한다.

 

2) 가상 함수 (Virtual Functions): 

BeginPlay(), Tick(), SetupPlayerInputComponent() 등의 엔진 수명 주기(Lifecycle) 함수들을 오버라이딩(Overriding)하여 게임 로직을 구현하게 된다.

 

2. 포인터(Pointer)와 참조(Reference)

게임 개발에서는 성능과 메모리 관리가 중요하기 때문에, 객체를 주고받을 때 포인터와 참조를 자주 사용하게 된다.

 

1) 안전한 포인터 사용: 

메모리 주소를 다루는 방법, 특히 널 포인터(Null Pointer) 체크는 게임 크래시(Crash)를 막기 위한 1순위 습관이 되어야 한다. (예: if (MyActor != nullptr))

 

2) 전방 선언 (Forward Declaration): 

컴파일 시간을 크게 줄이고 헤더 파일 간의 순환 참조를 방지하기 위해, 헤더 파일에서 포인터를 사용할 때 클래스를 전방 선언하는 기법(class UStaticMeshComponent;)이 매우 자주 쓰인다.

 

3. 언리얼 매크로와 리플렉션 (Reflection) 시스템

이 부분이 표준 C++과 가장 크게 다른 점이다. 언리얼 헤더 툴(UHT)이 코드에 작성된 매크로를 분석해 블루프린트와의 연동, 네트워크 동기화 등을 자동으로 처리하는 시스템이다.

 

1) UCLASS(), GENERATED_BODY(): 일반 C++ 클래스를 언리얼 객체 시스템에 등록한다.

 

2) UPROPERTY(): 변수를 에디터에 노출하거나 블루프린트에서 읽고 쓰게 만들며, 가장 중요한 가비지 컬렉션의 대상이 되도록 메모리를 보호한다.

 

3) UFUNCTION(): C++ 함수를 블루프린트에서 호출할 수 있게 하거나, 네트워크 통신(RPC) 및 델리게이트에 바인딩할 때 사용한다.

 

4. 메모리 관리 (가비지 컬렉션)

표준 C++에서는 new와 delete로 메모리를 직접 할당하고 해제하지만, 언리얼에서는 이를 지양해야 한다.

 

1) 표준 스마트 포인터 사용 주의: 

UObject를 상속받은 클래스는 언리얼의 가비지 컬렉터가 메모리를 관리하므로 std::shared_ptr 등을 사용하면 안된다!

 

2) 올바른 객체 생성 방법:

new 키워드 대신 액터(Actor)는 GetWorld()->SpawnActor<T>(), 일반 UObject는 NewObject<T>() 함수를 사용하여 생성해야 메모리 누수를 막을 수 있다.

 

5. 언리얼 전용 자료구조 (컨테이너)

표준 템플릿 라이브러리(STL)인 std::vector, std::string 대신 언리얼에서 게임 환경에 맞춰 자체 최적화한 컨테이너를 사용해야 한다.

 

1) TArray: 

동적 배열로 std::vector와 유사하며, 가장 널리 사용된다.

 

2) TMap / TSet: 

키-값 쌍의 맵과 고유한 값들의 집합이다.

 

3) 문자열 클래스: 

용도에 따라 세 가지로 나뉩니다. 데이터 조작용 FString, 빠른 검색 및 ID용 FName, 다국어 지원 및 UI 출력용 FText를 명확히 구분해서 사용해야 한다.

 

6. 델리게이트 (Delegates)

이벤트 주도형(Event-Driven) 프로그래밍의 핵심이다. 특정 조건이 달성되었을 때 다른 객체에 알림을 보내 함수를 실행(Callback)시키는 디자인 패턴이다.

 

UI 버튼 클릭, 타이머 종료, 데미지 피격 등 객체 간의 결합도를 낮추면서 안전하게 이벤트를 전달할 때 필수적으로 사용된다. (싱글캐스트, 멀티캐스트, 다이내믹 델리게이트 등)


[atanny]

 

1. 클라이언트-서버와 P2P 모델 구조

 

클라이언트-서버 모델 

중앙 서버가 클라이언트의 요청을 받아 처리하고 응답을 제공하는 구조이다. 클라이언트-서버 모델은 실제로는 모든 트래픽이 서버로 집중되어 서버 부하가 높다는 특징이 있다.

 

P2P(Peer-to-Peer)모델 

P2P모델은 중앙 서버가 없거나 최소한의 서버만 필요하기 때문에 서버 유지 비용이 클라이언트-서버 모델보다 낮다. 각 노드가 동등한 역할을 하며, 자원이 분산되어 확장성이 좋다는 것이 P2P의 주요 장점이다.

 

클라이언트-서버 모델의 단점 

중앙 서버에 대한 의존성이다. 서버에 장애가 발생하면 전체 서비스가 중단될 수 있으나, 오히려 중앙 집중식 보안 관리가 용이하다는 장점이 있다.

 

2. Listen Server와 Dedicated Server

 

Listen Server와 Dedicated Server의 구조적 차이 

Listen Server는 플레이어 중 한 명(호스트)이 게임 클라이언트를 실행하면서 동시에 서버 역할을 수행하는 구조이다.

Dedicated Server는 게임 플레이를 하지 않고 오직 서버 기능만 수행하는 독립된 프로세스이다.

 

Dedicated Server의 장점 

별도의 서버 하드웨어나 클라우드 인프라가 필요하므로 초기 비용과 유지비용이 상당히 발생한다.

다만 장점으로는, 호스트 플레이어의 네트워크 상태에 영향을 받지 않아 안정적인 게임 환경을 제공할 수 있으며, 서버를 24시간 운영할 수 있어 지속적인 게임 세션 유지가 가능하다. 또한 중앙 집중식 관리로 치트 방지 및 보안 관리가 용이하다.

 

Listen Server의 특징 

호스트 플레이어는 로컬에서 실행되므로 네트워크 지연이 없는 이점을 가진다. 호스트는 서버와 같은 머신에서 실행되어 네트워크 통신 없이 로컬에서 게임 상태에 접근할 수 있기 때문이다.


[이력서 세션 및 커리어 데이]

 

노션에 주요 내용들을 정리하고, 이력서에 들어갈 활동들을 했던 기간들을 찾아서 정리했다(작성해 놓았던 예전 이력서 파일 들과 일했던 자료들이 관리가 미흡해서 다 날아가버린것을 확인. 내용을 다시 작성하거나 아예 기억이 나지 않아서 사용을 못할 것 같다).


*오늘의 총평*

C++ 코드 공부를 더욱 열심히 하고 체계적으로 공부하고, 1) 템플릿과 클래스 등 좀 더 굵직굵직한 기능들도 하루에 2개씩 코딩테스트와 함께 공부하기 2) 언리얼 C++ 작성법도 중요하니까 하루에 하나씩이라도 직접 작성해보기 정도로 학습 전략을 추가했다.

 

이력서 작성할 때 채용공고를 보니까 막막한 기분이 들었는데, 지금으로서는 당연한 것이니 내가 모자란 부분을 계속 찾아내고 좋은 학습 태도를 유지하기 위해 마음을 다잡기로 했다. 최대한 적극적으로 프로젝트에 참여하고, 개인 공부도 계획을 세워서 차근차근 해내고, 내가 한 일들을 잘 정리해 놓아서 결정적인 순간에 성실하고 좋은 모습을 아주 제대로 보여줄 수 있도록 준비해야겠다.

 

'TIL' 카테고리의 다른 글

06.04 TIL (Ch.3 개인 과제 발제)  (0) 2026.06.04
06.02 TIL (커리어 데이)  (0) 2026.06.02
05.29 TIL (ch.2 팀프로젝트 day6)  (0) 2026.05.29
05.28 TIL (ch.2 팀프로젝트 day5)  (0) 2026.05.28
05.27 TIL (ch.2 팀프로젝트 day4)  (0) 2026.05.27