C언어로 그래프와 트리의 차이점 명확히 이해하기

그래프와 트리는 자료구조에서 핵심적인 개념으로, 데이터의 관계를 시각적으로 표현하고 다양한 문제를 해결하는 데 사용됩니다. 이 기사에서는 C언어를 활용해 그래프와 트리의 차이점을 이해하고, 이들을 구현하고 활용하는 방법을 학습합니다.

목차

그래프란 무엇인가


그래프는 노드(정점)와 엣지(간선)로 구성된 자료구조로, 데이터 간의 관계를 표현하는 데 사용됩니다. 노드는 데이터를 나타내고, 엣지는 노드 간의 관계를 나타냅니다. 그래프는 다음과 같은 유형으로 나뉩니다.

그래프의 유형

  • 유향 그래프: 간선이 방향성을 가지며, 한쪽 방향으로만 이동 가능합니다.
  • 무향 그래프: 간선이 방향성을 가지지 않아 양방향 이동이 가능합니다.
  • 가중치 그래프: 간선에 가중치가 부여되어 이동 비용이나 거리를 나타냅니다.
  • 비가중치 그래프: 간선에 가중치가 없는 그래프입니다.

그래프의 표현 방법


C언어에서 그래프는 일반적으로 다음 두 가지 방식으로 구현됩니다.

  1. 인접 행렬: 2차원 배열을 사용해 노드 간 연결 여부를 저장합니다.
  2. 인접 리스트: 연결 리스트를 사용해 각 노드의 이웃 노드를 저장합니다.

예시 코드: 인접 리스트를 이용한 그래프 구현

#include <stdio.h>
#include <stdlib.h>

// 노드 구조체 정의
typedef struct Node {
    int vertex;
    struct Node* next;
} Node;

// 그래프 구조체 정의
typedef struct Graph {
    int numVertices;
    Node** adjLists;
} Graph;

// 그래프 생성 함수
Graph* createGraph(int vertices) {
    Graph* graph = malloc(sizeof(Graph));
    graph->numVertices = vertices;
    graph->adjLists = malloc(vertices * sizeof(Node*));
    for (int i = 0; i < vertices; i++)
        graph->adjLists[i] = NULL;
    return graph;
}

// 간선 추가 함수
void addEdge(Graph* graph, int src, int dest) {
    Node* newNode = malloc(sizeof(Node));
    newNode->vertex = dest;
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;
}

// 그래프 출력 함수
void printGraph(Graph* graph) {
    for (int i = 0; i < graph->numVertices; i++) {
        Node* temp = graph->adjLists[i];
        printf("Vertex %d:\n", i);
        while (temp) {
            printf(" -> %d", temp->vertex);
            temp = temp->next;
        }
        printf("\n");
    }
}

int main() {
    Graph* graph = createGraph(4);
    addEdge(graph, 0, 1);
    addEdge(graph, 0, 2);
    addEdge(graph, 1, 2);
    addEdge(graph, 2, 3);
    printGraph(graph);
    return 0;
}

그래프는 다양한 알고리즘에서 사용되며, 특히 네트워크 모델링, 경로 탐색, 소셜 네트워크 분석 등에서 중요한 역할을 합니다.

트리란 무엇인가


트리는 그래프의 한 유형으로, 계층적 데이터를 표현하는 데 사용되는 자료구조입니다. 트리는 노드와 간선으로 구성되며, 다음과 같은 주요 특징을 갖습니다.

트리의 주요 특징

  • 루트 노드: 트리의 시작점이며, 최상위 노드를 의미합니다.
  • 자식 노드: 특정 노드에 직접 연결된 하위 노드입니다.
  • 부모 노드: 특정 노드의 상위 노드입니다.
  • 단순 경로: 루트에서 리프 노드까지의 유일한 경로를 갖습니다.
  • 사이클 없음: 트리는 사이클이 존재하지 않는 비순환 그래프입니다.

트리의 유형

  • 이진 트리: 각 노드가 최대 두 개의 자식을 가질 수 있는 트리입니다.
  • 이진 검색 트리(BST): 이진 트리의 일종으로, 왼쪽 자식은 부모보다 작고, 오른쪽 자식은 부모보다 큰 값을 가집니다.
  • AVL 트리: 균형을 유지하는 이진 검색 트리로, 삽입 및 삭제 시 스스로 균형을 조정합니다.
  • 힙(Heap): 완전 이진 트리로, 특정 순서를 유지하며 우선순위 큐를 구현하는 데 사용됩니다.

트리의 구현 방법


C언어에서 트리는 일반적으로 연결 리스트 또는 구조체를 사용하여 구현됩니다. 아래는 이진 검색 트리의 예시 코드입니다.

예시 코드: 이진 검색 트리 구현

#include <stdio.h>
#include <stdlib.h>

// 노드 구조체 정의
typedef struct Node {
    int data;
    struct Node* left;
    struct Node* right;
} Node;

// 새로운 노드 생성 함수
Node* createNode(int data) {
    Node* newNode = malloc(sizeof(Node));
    newNode->data = data;
    newNode->left = newNode->right = NULL;
    return newNode;
}

// 노드 삽입 함수
Node* insert(Node* root, int data) {
    if (root == NULL) return createNode(data);
    if (data < root->data)
        root->left = insert(root->left, data);
    else
        root->right = insert(root->right, data);
    return root;
}

// 중위 순회 함수
void inorderTraversal(Node* root) {
    if (root == NULL) return;
    inorderTraversal(root->left);
    printf("%d ", root->data);
    inorderTraversal(root->right);
}

int main() {
    Node* root = NULL;
    root = insert(root, 50);
    insert(root, 30);
    insert(root, 70);
    insert(root, 20);
    insert(root, 40);
    insert(root, 60);
    insert(root, 80);

    printf("Inorder Traversal: ");
    inorderTraversal(root);
    printf("\n");

    return 0;
}

트리는 파일 시스템, 데이터베이스 인덱싱, 네트워크 라우팅 등 다양한 분야에서 사용되며, 데이터의 계층적 구조를 효과적으로 표현합니다.

그래프와 트리의 주요 차이점

그래프와 트리는 모두 노드와 간선으로 구성된 자료구조이지만, 구조적 특징과 사용 용도에서 몇 가지 중요한 차이점이 있습니다.

구조적 차이점

  • 사이클 유무:
  • 그래프는 사이클을 포함할 수 있습니다.
  • 트리는 사이클이 없는 비순환 그래프입니다.
  • 루트 노드:
  • 그래프는 특정 시작점(루트 노드)을 요구하지 않습니다.
  • 트리는 루트 노드에서 시작하여 하위 노드로 확장됩니다.
  • 간선의 수:
  • 트리는 항상 (N-1)개의 간선을 가집니다((N)은 노드의 수).
  • 그래프는 간선의 수에 제한이 없으며, 밀도에 따라 다릅니다.

데이터 관계의 차이점

  • 계층적 관계:
  • 트리는 계층적 구조를 표현합니다. 예: 조직도, 파일 시스템.
  • 그래프는 일반적인 관계를 표현하며 계층 구조가 필요하지 않습니다. 예: 소셜 네트워크, 지도 데이터.
  • 연결성:
  • 트리는 모든 노드가 정확히 하나의 경로로 연결됩니다.
  • 그래프는 모든 노드가 연결되지 않을 수도 있으며, 다중 경로를 허용합니다.

사용 용도의 차이점

  • 트리:
  • 데이터 검색: 이진 검색 트리(BST)와 같은 구조를 통해 효율적인 데이터 검색 가능.
  • 계층적 데이터 표현: 파일 디렉터리 구조, XML/HTML DOM 구조.
  • 그래프:
  • 네트워크 분석: 소셜 네트워크, 인터넷 라우팅.
  • 경로 탐색: 최단 경로 알고리즘(Dijkstra, Floyd-Warshall).

비교 표

특성트리그래프
간선의 수(N-1)제한 없음
사이클없음있을 수 있음
계층적 구조항상 있음있을 수도 있고 없을 수도 있음
연결성모든 노드 연결모든 노드가 연결되지 않을 수도 있음
주요 용도계층적 데이터 표현네트워크 및 관계 표현

이러한 차이점을 이해하면, 특정 문제를 해결할 때 어떤 자료구조를 사용할지 명확하게 결정할 수 있습니다.

그래프와 트리의 활용 사례

그래프와 트리는 각각의 고유한 특성으로 인해 다양한 분야에서 활용됩니다. 이들은 데이터의 관계를 시각적으로 표현하고, 문제를 해결하는 데 중요한 역할을 합니다.

그래프의 활용 사례

  1. 네트워크 분석
  • 인터넷 라우팅: 네트워크 노드 간 데이터 전송 경로 최적화.
  • 소셜 네트워크 분석: 사용자 간의 연결성과 영향력 분석.
  1. 경로 탐색 문제
  • 지도 및 내비게이션: 최단 경로 찾기(Dijkstra, A* 알고리즘).
  • 전력망 설계: 최소 신장 트리(MST)를 사용한 네트워크 비용 최적화.
  1. 프로젝트 관리
  • 작업 흐름 다이어그램: 작업의 종속성을 그래프로 표현하여 일정 관리.

트리의 활용 사례

  1. 계층적 데이터 표현
  • 파일 시스템: 디렉터리 구조를 트리 형태로 저장.
  • HTML/XML DOM: 웹 문서의 요소를 트리로 구성.
  1. 데이터 검색 및 정렬
  • 이진 검색 트리(BST): 정렬된 데이터를 빠르게 검색.
  • AVL 트리와 레드-블랙 트리: 균형을 유지하며 효율적인 삽입과 삭제 지원.
  1. 우선순위 관리
  • 힙(Heap): 우선순위 큐 구현으로 작업 스케줄링 및 최적화 문제 해결.

응용 사례 비교

사례그래프 활용트리 활용
네비게이션 시스템경로 탐색 및 최단 거리 계산계층적 목적지 관리
데이터베이스 인덱싱복잡한 관계 테이블 관리B-트리 및 B+트리를 통한 데이터 인덱싱
소셜 네트워크 분석연결성과 클러스터 분석트리 구조 기반의 권한 관리

이처럼 그래프와 트리는 특정 문제에 따라 각각 적합한 방식으로 사용됩니다. 문제의 요구사항에 따라 올바른 자료구조를 선택하는 것이 중요합니다.

C언어로 그래프와 트리 구현하기

C언어를 사용하여 그래프와 트리를 구현하는 것은 자료구조와 알고리즘 이해를 심화하는 데 큰 도움이 됩니다. 아래에서는 두 자료구조의 기본 구현 방법을 소개합니다.

그래프 구현


그래프는 일반적으로 인접 행렬인접 리스트 두 가지 방식으로 표현됩니다.

인접 행렬 구현


인접 행렬은 2차원 배열을 사용하여 그래프를 표현합니다.

#include <stdio.h>

#define MAX_VERTICES 5

void printAdjMatrix(int adjMatrix[MAX_VERTICES][MAX_VERTICES]) {
    for (int i = 0; i < MAX_VERTICES; i++) {
        for (int j = 0; j < MAX_VERTICES; j++) {
            printf("%d ", adjMatrix[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int adjMatrix[MAX_VERTICES][MAX_VERTICES] = {0};

    // 간선 추가
    adjMatrix[0][1] = 1;
    adjMatrix[1][2] = 1;
    adjMatrix[2][3] = 1;

    printf("인접 행렬 표현:\n");
    printAdjMatrix(adjMatrix);

    return 0;
}

인접 리스트 구현


인접 리스트는 노드의 연결을 연결 리스트로 표현합니다.

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int vertex;
    struct Node* next;
} Node;

typedef struct Graph {
    int numVertices;
    Node** adjLists;
} Graph;

Graph* createGraph(int vertices) {
    Graph* graph = malloc(sizeof(Graph));
    graph->numVertices = vertices;
    graph->adjLists = malloc(vertices * sizeof(Node*));

    for (int i = 0; i < vertices; i++)
        graph->adjLists[i] = NULL;

    return graph;
}

void addEdge(Graph* graph, int src, int dest) {
    Node* newNode = malloc(sizeof(Node));
    newNode->vertex = dest;
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;
}

void printGraph(Graph* graph) {
    for (int i = 0; i < graph->numVertices; i++) {
        Node* temp = graph->adjLists[i];
        printf("Vertex %d:", i);
        while (temp) {
            printf(" -> %d", temp->vertex);
            temp = temp->next;
        }
        printf("\n");
    }
}

int main() {
    Graph* graph = createGraph(5);
    addEdge(graph, 0, 1);
    addEdge(graph, 0, 4);
    addEdge(graph, 1, 2);

    printf("인접 리스트 표현:\n");
    printGraph(graph);

    return 0;
}

트리 구현


트리는 노드 구조체와 재귀를 사용하여 구현합니다.

이진 트리 구현

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node* left;
    struct Node* right;
} Node;

Node* createNode(int data) {
    Node* newNode = malloc(sizeof(Node));
    newNode->data = data;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

void inorderTraversal(Node* root) {
    if (root == NULL) return;
    inorderTraversal(root->left);
    printf("%d ", root->data);
    inorderTraversal(root->right);
}

int main() {
    Node* root = createNode(1);
    root->left = createNode(2);
    root->right = createNode(3);
    root->left->left = createNode(4);
    root->left->right = createNode(5);

    printf("중위 순회 결과: ");
    inorderTraversal(root);
    printf("\n");

    return 0;
}

C언어를 통해 그래프와 트리를 구현하면 자료구조의 기본 원리를 체계적으로 이해하고, 효율적인 알고리즘 설계 능력을 키울 수 있습니다.

그래프와 트리의 시간 복잡도 비교

그래프와 트리는 서로 다른 특성과 구조를 가지고 있어, 연산 시 시간 복잡도에도 차이가 있습니다. 아래에서는 각 자료구조에서 일반적으로 발생하는 연산의 시간 복잡도를 분석합니다.

그래프의 시간 복잡도


그래프의 시간 복잡도는 그래프의 표현 방식(인접 행렬 또는 인접 리스트)과 그래프의 밀도(간선의 수)에 따라 달라집니다.

  1. 그래프의 탐색
  • 인접 행렬: (O(V^2)) (모든 노드와 간선을 검사)
  • 인접 리스트: (O(V + E)) (모든 노드와 연결된 간선 검사)
  • 탐색 알고리즘: BFS와 DFS 모두 인접 리스트 기준으로 (O(V + E))입니다.
  1. 간선 추가
  • 인접 행렬: (O(1)) (행렬 값 업데이트)
  • 인접 리스트: (O(1)) (새로운 노드 추가)
  1. 간선 검사
  • 인접 행렬: (O(1))
  • 인접 리스트: (O(k)) (노드의 연결 리스트 크기 (k))

트리의 시간 복잡도


트리는 그 구조적 특징(계층적 구조와 (N-1)개의 간선)으로 인해 시간 복잡도가 비교적 간단합니다.

  1. 탐색 (DFS 또는 BFS)
  • 이진 트리: (O(N)) (모든 노드를 방문)
  • 완전 이진 트리: (O(\log N)) (높이에 비례한 탐색)
  1. 삽입과 삭제
  • 이진 검색 트리(BST):
    • 평균: (O(\log N))
    • 최악: (O(N)) (비균형 트리의 경우)
  • AVL 트리: (O(\log N)) (균형 유지)
  1. 검색
  • BST: (O(\log N))
  • 균형 트리: (O(\log N))

그래프와 트리의 연산 비교

연산그래프 (인접 리스트)트리
탐색(O(V + E))(O(N))
간선 추가(O(1))해당 없음
간선 검사(O(k))해당 없음
노드 삽입해당 없음(O(\log N)) (BST)
노드 삭제해당 없음(O(\log N)) (BST)

시간 복잡도에 따른 자료구조 선택

  • 그래프는 데이터 관계가 복잡하거나 간선 수가 많은 경우에 적합합니다.
  • 트리는 데이터가 계층적 구조를 따르며, 균형 있는 검색이나 정렬이 필요한 경우에 적합합니다.

문제의 요구 사항에 따라 적합한 자료구조를 선택하면 성능과 효율성을 극대화할 수 있습니다.

문제 해결: 트리와 그래프를 활용한 경로 찾기

트리와 그래프는 데이터 간의 관계를 기반으로 경로를 탐색하거나 최적의 경로를 찾는 데 자주 사용됩니다. 아래에서는 두 자료구조를 활용하여 경로 탐색 문제를 해결하는 방법과 알고리즘을 소개합니다.

트리를 활용한 경로 탐색

트리는 계층적 구조이므로 특정 노드 간의 경로 탐색은 재귀적 접근이 유용합니다.
예를 들어, 이진 트리에서 두 노드 간의 공통 경로를 찾는 문제를 생각해볼 수 있습니다.

예시: 이진 트리의 최소 공통 조상(LCA) 찾기

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node* left;
    struct Node* right;
} Node;

Node* createNode(int data) {
    Node* newNode = malloc(sizeof(Node));
    newNode->data = data;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

Node* findLCA(Node* root, int n1, int n2) {
    if (root == NULL) return NULL;
    if (root->data == n1 || root->data == n2) return root;

    Node* leftLCA = findLCA(root->left, n1, n2);
    Node* rightLCA = findLCA(root->right, n1, n2);

    if (leftLCA && rightLCA) return root;
    return (leftLCA != NULL) ? leftLCA : rightLCA;
}

int main() {
    Node* root = createNode(1);
    root->left = createNode(2);
    root->right = createNode(3);
    root->left->left = createNode(4);
    root->left->right = createNode(5);

    Node* lca = findLCA(root, 4, 5);
    if (lca != NULL) printf("LCA: %d\n", lca->data);
    else printf("LCA not found\n");

    return 0;
}

이 알고리즘은 (O(N))의 시간 복잡도를 가지며, 이진 트리에서 두 노드 간 공통 경로를 탐색합니다.

그래프를 활용한 최단 경로 탐색

그래프는 여러 경로를 탐색할 수 있으므로, 최단 경로를 찾는 데 유용합니다.
대표적인 알고리즘으로는 다익스트라 알고리즘벨만-포드 알고리즘이 있습니다.

예시: 다익스트라 알고리즘을 이용한 최단 경로 찾기

#include <stdio.h>
#include <limits.h>

#define V 5
#define INF INT_MAX

int findMinDistance(int dist[], int visited[]) {
    int min = INF, minIndex;
    for (int v = 0; v < V; v++) {
        if (!visited[v] && dist[v] <= min) {
            min = dist[v];
            minIndex = v;
        }
    }
    return minIndex;
}

void dijkstra(int graph[V][V], int src) {
    int dist[V];
    int visited[V] = {0};

    for (int i = 0; i < V; i++) dist[i] = INF;
    dist[src] = 0;

    for (int count = 0; count < V - 1; count++) {
        int u = findMinDistance(dist, visited);
        visited[u] = 1;

        for (int v = 0; v < V; v++) {
            if (!visited[v] && graph[u][v] && dist[u] != INF && dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
            }
        }
    }

    printf("Vertex\tDistance from Source\n");
    for (int i = 0; i < V; i++) {
        printf("%d\t%d\n", i, dist[i]);
    }
}

int main() {
    int graph[V][V] = {
        {0, 2, 0, 0, 0},
        {2, 0, 3, 0, 0},
        {0, 3, 0, 7, 1},
        {0, 0, 7, 0, 2},
        {0, 0, 1, 2, 0}
    };

    dijkstra(graph, 0);
    return 0;
}

이 알고리즘은 (O(V^2))의 시간 복잡도를 가지며, 인접 행렬을 사용하여 그래프의 최단 경로를 찾습니다.

문제 해결 시 고려 사항

  • 트리는 특정 구조적 문제(계층적 관계)에 적합합니다.
  • 그래프는 다중 경로나 복잡한 연결을 다룰 때 유용합니다.
  • 문제의 성격에 따라 적합한 알고리즘을 선택해야 합니다.

이러한 접근법은 트리와 그래프를 효과적으로 활용하여 다양한 경로 탐색 문제를 해결하는 데 도움이 됩니다.

실습: C언어로 그래프와 트리 문제 풀기

이 섹션에서는 앞서 배운 그래프와 트리 구현 기술을 활용해, 실습 문제를 해결하며 이해를 심화합니다.

문제 1: 그래프에서 특정 노드까지의 최단 거리 계산


설명: 주어진 그래프에서 시작 노드와 목표 노드 사이의 최단 거리를 구합니다.
다익스트라 알고리즘을 사용하여 문제를 해결합니다.

코드 예제

#include <stdio.h>
#include <limits.h>

#define V 6
#define INF INT_MAX

int findMinDistance(int dist[], int visited[]) {
    int min = INF, minIndex = -1;
    for (int i = 0; i < V; i++) {
        if (!visited[i] && dist[i] <= min) {
            min = dist[i];
            minIndex = i;
        }
    }
    return minIndex;
}

void findShortestPath(int graph[V][V], int src, int target) {
    int dist[V];
    int visited[V] = {0};

    for (int i = 0; i < V; i++) dist[i] = INF;
    dist[src] = 0;

    for (int count = 0; count < V - 1; count++) {
        int u = findMinDistance(dist, visited);
        visited[u] = 1;

        for (int v = 0; v < V; v++) {
            if (!visited[v] && graph[u][v] && dist[u] != INF && dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
            }
        }
    }

    printf("최단 거리 (노드 %d에서 노드 %d까지): %d\n", src, target, dist[target]);
}

int main() {
    int graph[V][V] = {
        {0, 1, 4, 0, 0, 0},
        {1, 0, 4, 2, 7, 0},
        {4, 4, 0, 3, 5, 0},
        {0, 2, 3, 0, 4, 6},
        {0, 7, 5, 4, 0, 7},
        {0, 0, 0, 6, 7, 0}
    };

    int src = 0, target = 4;
    findShortestPath(graph, src, target);
    return 0;
}

결과: 주어진 노드 간의 최단 거리가 출력됩니다.


문제 2: 트리에서 두 노드 간의 경로 출력


설명: 이진 트리에서 루트부터 특정 노드까지의 경로를 출력합니다.
재귀를 사용하여 경로를 탐색합니다.

코드 예제

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node* left;
    struct Node* right;
} Node;

Node* createNode(int data) {
    Node* newNode = malloc(sizeof(Node));
    newNode->data = data;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

int findPath(Node* root, int target, int path[], int pathLength) {
    if (root == NULL) return 0;

    path[pathLength] = root->data;
    pathLength++;

    if (root->data == target) {
        for (int i = 0; i < pathLength; i++) {
            printf("%d ", path[i]);
        }
        printf("\n");
        return 1;
    }

    if (findPath(root->left, target, path, pathLength) || findPath(root->right, target, path, pathLength)) {
        return 1;
    }

    return 0;
}

int main() {
    Node* root = createNode(1);
    root->left = createNode(2);
    root->right = createNode(3);
    root->left->left = createNode(4);
    root->left->right = createNode(5);

    int path[100];
    int target = 5;

    printf("노드 %d까지의 경로: ", target);
    if (!findPath(root, target, path, 0)) {
        printf("노드를 찾을 수 없습니다.\n");
    }

    return 0;
}

결과: 특정 노드까지의 경로가 출력됩니다.


실습 요약

  • 그래프 문제: 다익스트라 알고리즘을 사용해 특정 노드 간의 최단 거리를 계산했습니다.
  • 트리 문제: 재귀를 사용하여 루트에서 특정 노드까지의 경로를 출력했습니다.

이러한 실습은 그래프와 트리의 실제 사용 사례를 이해하고, 문제 해결 능력을 향상시키는 데 도움을 줍니다.

요약


이 기사에서는 C언어를 활용해 그래프와 트리의 차이점, 구현 방법, 시간 복잡도 분석, 그리고 실질적인 문제 해결 방법을 다루었습니다. 그래프는 복잡한 관계를 표현하는 데 유용하며, 트리는 계층적 데이터 관리에 적합합니다. 다양한 알고리즘과 코드를 통해 실습하며, 이 두 자료구조를 효과적으로 활용하는 방법을 배웠습니다.

목차