프로그래머스 데브코스TIL

[week9] 프론트엔드 기초 : React + TypeScript (5)

이규현2026-03-13
[week9] 프론트엔드 기초 : React + TypeScript (5)

함수 포인터

함수 포인터는 함수의 메모리 주소를 저장하는 포인터입니다. 일반 포인터가 변수의 주소를 담듯, 함수 포인터는 함수의 주소를 담아 어떤 함수를 호출할지 런타임에 동적으로 결정할 수 있게 해줍니다. 대표적인 활용은 특정 조건이 됐을 때 미리 등록한 함수를 호출하는 콜백(Callback) 패턴입니다.

반환타입 (*포인터이름)(매개변수타입);
#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main() {
    int (*op)(int, int);  // 함수 포인터 선언

    op = add;
    printf("add: %d\n", op(3, 2));  // 5

    op = sub;
    printf("sub: %d\n", op(3, 2));  // 1

    return 0;
}

구조체와 공용체

구조체는서로 다른 타입의 변수들을 하나의 논리적 단위로 묶는 사용자 정의 자료형입니다. 메모리상에서 각 멤버를 순서대로 연속 배치하며, 컴파일러가 정렬 효율을 위해 패딩(Padding) 을 삽입할 수 있어 실제 크기는 멤버 합보다 클 수 있습니다.

공용체는 모든 멤버가 동일한 메모리 공간을 공유하는 자료형입니다. 가장 큰 멤버 크기만큼만 메모리를 차지하며, 한 멤버에 값을 쓰면 나머지는 의미를 잃습니다. 메모리가 제한된 임베디드 환경이나 같은 데이터를 여러 타입으로 해석해야 할 때 주로 사용합니다.

열거형 (enum)

관련된 정수 상수들에 의미 있는 이름을 붙여 그룹으로 묶은 자료형입니다. 코드 속에 의미 없이 등장하는 숫자(매직 넘버)를 없애고, 가독성과 유지보수성을 높여줍니다. if (status == 1) 대신 if (status == PENDING)처럼 쓸 수 있습니다.

enum Day { MON, TUE, WED, THU, FRI, SAT, SUN };  // 0부터 자동 할당

enum Status {
    SUCCESS =  0,
    ERROR   = -1,
    PENDING =  1
};

동적 메모리 할당

프로그램 실행 중에 필요한 크기만큼 힙(Heap) 메모리를 직접 요청하고 관리하는 방법입니다.

스택(Stack)은 크기가 컴파일 타임에 고정되어야 하지만, 힙은 실행 중에 크기를 결정할 수 있습니다. 단, 사용 후 반드시 직접 해제해야 하며, 해제하지 않으면 메모리 누수(Memory Leak) 가 발생합니다. 장시간 실행되는 프로그램에서 누수가 쌓이면 결국 메모리 부족으로 다운될 수 있습니다.

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

현실 세계의 개념을 객체(Object)로 모델링하여 프로그램을 구성하는 패러다임입니다.

절차지향이 "어떤 순서로 실행할까?"를 중심으로 생각한다면, OOP는 "어떤 객체들이 협력할까?" 를 중심으로 생각합니다. 데이터(속성)와 기능(행동)을 하나의 객체로 묶기 때문에, 관련 코드가 한 곳에 모여 수정 범위가 좁아지고 재사용성이 높아집니다.

OOP의 핵심 4가지 특성

특성설명
캡슐화데이터와 메서드를 묶고 외부 접근을 제한
추상화필요한 인터페이스만 노출, 내부는 숨김
상속성부모 클래스의 속성/기능을 자식이 물려받음
다형성같은 인터페이스로 다양한 동작 수행

추상화

추상화는 복잡한 내부 구현은 숨기고, 필요한 인터페이스만 노출하는 개념입니다.

자동차를 운전할 때 엔진 내부 구조를 몰라도 핸들과 페달만 알면 운전할 수 있듯, 추상화는 사용자가 "어떻게 동작하는지" 몰라도 "무엇을 할 수 있는지"만 알면 쓸 수 있게 해줍니다. 내부 구현이 바뀌어도 인터페이스가 동일하면 사용하는 쪽 코드는 수정할 필요가 없다는 것도 큰 장점입니다.

캡슐화

캡슐화는 데이터와 기능을 하나로 묶고, 외부에서 데이터에 직접 접근하지 못하도록 보호하는 개념입니다.

외부에서 객체 내부를 마음대로 바꿀 수 있다면 잔액이 음수가 되거나, 나이가 -10이 되는 등 잘못된 상태가 만들어질 수 있습니다. 캡슐화는 데이터 변경을 검증 로직이 담긴 메서드를 통해서만 허용해 데이터 무결성을 보장합니다.

접근 제어자접근 범위
public어디서든 접근 가능
protected자신 + 자식 클래스
private자신만

클래스의 기본

클래스는 객체를 만들기 위한 설계도입니다. 붕어빵 틀(클래스)로 붕어빵(객체)을 여러 개 찍어내듯, 하나의 클래스로 여러 객체(인스턴스)를 생성할 수 있습니다. 클래스 자체는 메모리를 차지하지 않으며, 객체를 생성하는 순간 메모리가 할당됩니다.

  • 멤버 변수(속성): 객체가 가지는 데이터. 각 객체마다 독립적인 값을 가집니다.
  • 멤버 함수(메서드): 객체가 수행할 수 있는 동작입니다.
class Car {
private:
    string brand;
    int speed;

public:
    Car(string b, int s) : brand(b), speed(s) {}
    void accelerate(int amount) { speed += amount; }
    void info() const { cout << brand << ": " << speed << "km/h" << endl; }
};

Car car1("현대", 0);  // 객체 생성
car1.accelerate(60);
car1.info();          // 현대: 60km/h

생성자

생성자는 객체가 생성될 때 자동으로 호출되는 초기화 전용 함수입니다. 클래스 이름과 동일하고 반환 타입이 없습니다.

생성자가 실행을 마치면 객체는 항상 올바른 초기 상태를 보장받습니다. 생성자를 직접 정의하지 않으면 컴파일러가 기본 생성자를 자동으로 만들지만, 하나라도 정의하면 자동 생성이 중단됩니다.

생성자의 종류

종류설명
기본 생성자매개변수 없이 호출
매개변수 생성자초기값을 직접 전달
복사 생성자같은 타입의 객체로 초기화

상속성

상속은 부모 클래스의 속성과 기능을 자식 클래스가 물려받아 재사용하고 확장하는 개념입니다.

공통 기능은 부모에 한 번만 작성하고, 자식은 차이점만 추가하면 됩니다. 상속은 "is-a" 관계를 표현합니다(Dog is an Animal). 이 덕분에 부모 타입 포인터로 자식 객체를 다룰 수 있어 다형성이 가능해집니다.

class Animal {
protected:
    string name;
public:
    Animal(string n) : name(n) {}
    void breathe() { cout << name << "이(가) 숨을 쉽니다.\n"; }
    virtual void speak() { cout << "...\n"; }
};

class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}
    void speak() override { cout << name << ": 멍멍!\n"; }
};

class Cat : public Animal {
public:
    Cat(string n) : Animal(n) {}
    void speak() override { cout << name << ": 야옹~\n"; }
};

// 다형성: 부모 포인터로 자식 관리
Animal *a[] = { new Dog("바둑이"), new Cat("나비") };
for (auto *x : a) x->speak();  // 멍멍! / 야옹~

오버로딩

오버로딩은 같은 이름의 함수를 매개변수의 타입이나 개수만 다르게 여러 버전으로 정의하는 것입니다.

오버로딩이 없다면 addInt(), addDouble(), addString()처럼 거의 같은 기능에 다른 이름을 붙여야 합니다. 컴파일러가 컴파일 타임에 인자의 타입과 개수를 보고 어떤 버전을 호출할지 결정합니다(정적 바인딩). 반환 타입만 다른 경우는 오버로딩이 불가능합니다.

오버라이딩

오버라이딩은 부모 클래스의 메서드를 자식 클래스에서 같은 시그니처로 재정의하는 것입니다.

오버로딩이 컴파일 타임에 결정되는 것과 달리, 오버라이딩은 런타임에 실제 객체 타입에 따라 어떤 메서드를 호출할지 결정됩니다(동적 바인딩). C++에서는 부모 함수에 virtual이 있어야 동적 바인딩이 작동하며, 자식 함수에 override를 붙이면 컴파일러가 올바른 재정의인지 검사해줍니다.

인터페이스

인터페이스는 클래스가 반드시 구현해야 할 메서드를 명세한 계약(Contract) 입니다. "어떻게 구현할지"가 아닌 "무엇을 구현해야 하는지" 만 선언합니다.

특정 클래스에 직접 의존하는 대신 인터페이스에 의존하면 결합도가 낮아져, 실제 구현이 바뀌어도 사용하는 쪽 코드는 수정할 필요가 없습니다. 예를 들어 결제 인터페이스를 만들면 카카오페이든 네이버페이든 인터페이스만 구현하면 기존 코드와 바로 연동됩니다.

C++에서는 순수 가상 함수만으로 구성된 추상 클래스로 구현하고, Java/C#에서는 interface 키워드를 사용합니다.

람다 (Lambda)

람다는 이름 없이 그 자리에서 바로 정의하고 사용할 수 있는 익명 함수입니다.

한 번만 쓸 함수를 위해 별도로 선언하는 번거로움을 없애고, 함수가 필요한 자리에 바로 작성할 수 있습니다. 또한 클로저(Closure) 특성 덕분에 자신이 선언된 스코프의 외부 변수를 캡처하여 함수 내부에서 사용할 수 있습니다.