유니티에서 오브젝트 풀 Object Pool 만들기 3 – 총알 발사하기

유니티에서 오브젝트 풀 Object Pool 만들기 3 – 총알 발사하기

object pool

 

오브젝트 풀 시리즈 전체

 

오브젝트 풀 사용하기

이번에는 지난 시간까지 만들었던 오브젝트 풀을 사용해서 마우스 왼쪽 버튼을 누르면 총알이 발사되는 간단한 예제를 만들어보겠습니다.

일반적으로 총알이 발사되려면 먼저 씬에 생성되고, 특정 시간 또는 영역을 벗어나면 다시 제거되는 과정을 거치게 됩니다.
하지만 이번 예제에서는 만들어 놓은 오브젝트 풀을 사용해서 객체의 생성/해제 과정 없이 객체를 재사용하는 방법을 통해서 총알을 발사 시켜보겠습니다.

 

Bullet 프리팹 만들기

총알을 오브젝트 풀에 넣고 재사용하도록 하기 위해서 프리팹으로 만들어 둡니다.

유니티에서 GameObject->3D Object->Cube 메뉴를 선택해서 Cube 게임 오브젝트를 생성합니다. 생성된 큐브의 이름을 Bullet으로 지정합니다.

cube creation

생성된 Bullet 게임오브젝트를 선택하고 Scale 값을 모두 0.3으로 입력합니다. 나중에 총알이 발사되었을 때 적당히 작게 보이기 위해서 0.3을 입력했습니다.
(크기가 마음에 들지 않으면 다른 값을 입력하셔도 됩니다.)

set scale

Cube 게임오브젝트 설정이 완료되었으면, 총알을 움직이기 위한 스크립트를 추가합니다. Create->C# Script 메뉴를 선택해서 스크립트를 생성하고, 스크립트 이름을 BulletMove로 지정합니다.
생성된 스크립트를 열고 아래 내용을 입력합니다.

using UnityEngine;
using System.Collections;

public class BulletMove : MonoBehaviour
{
    public string poolItemName = "Bullet";
    public float moveSpeed = 10f;
    public float lifeTime = 3f;
    public float _elapsedTime = 0f;

    void Update()
    {
        transform.position += transform.up * moveSpeed * Time.deltaTime;

        if (GetTimer() > lifeTime)
        {
            SetTimer();
            ObjectPool.Instance.PushToPool(poolItemName, gameObject);
        }
    }

    float GetTimer()
    {
        return (_elapsedTime += Time.deltaTime);
    }

    void SetTimer()
    {
        _elapsedTime = 0f;
    }
}

먼저 변수부터 내용을 살펴보겠습니다. 아래 내용을 참고해서 각 변수의 용도를 확인하시면 됩니다.

  • poolItemName : 오브젝트 풀에 저장된 bullet 오브젝트의 이름
  • moveSpeed : 총알의 이동 속도
  • lifeTime : 총알의 수명 (초단위)
  • _elapsedTime : 총알이 활성화된 뒤 경과시간을 계산하기 위한 변수

Update함수에서 총알을 위 방향으로 moveSpeed이동속도를 적용해서 이동시킵니다. 이 코드를 거치면 총알이 위로 발사됩니다.
그 다음에 GetTimer()함수에서 현재까지 경과된 시간을 확인하고, 경과 시간이 총알의 수명을 지났는지 확인합니다.

총알이 수명을 다하면, 타이머를 초기화하고, 오브젝트 풀에, 다 쓴 총알을 반환합니다. 여기에서 ObjectPool.Instance.PushToPool(); 함수를 사용합니다.

GetTimer()함수와 SetTimer()함수는 각각 경과시간을 확인하고, 초기화하는 역할을 합니다.

BulletMove스크립트 작성이 완료되면, Bullet게임오브젝트에 컴포넌트로 추가합니다.

add bulletmove script

컴포넌트 추가까지 완료되면, 총알의 형태가 갖춰졌습니다. 이제 하이어라키에 있는 Bullet 게임오브젝트를 프로젝트뷰로 드래그해서 프리팹을 생성합니다.
이 때 프로젝트뷰에 Prefabs 폴더를 만들어서 정리하는 것이 좋습니다.

creating bullet prefab

프리팹이 완성되면, 하이어라키에 있는 Bullet게임오브젝트를 삭제합니다.

 

오브젝트 풀 매니저 만들기

위에서 만든 프리팹을 이용해서 오브젝트 풀 매니저를 만들어 보겠습니다. 스크립트는 지난 두 번의 강좌를 통해서 이미 준비되어 있습니다.
놓치신 분들은 아래 링크를 참고해서 강좌를 보시고 다시 진행하시면 됩니다.

PooledObject스크립트와 ObjectPool스크립트가 준비되어 있다고 가정하고 진행합니다. 이제 매니저를 만들겠습니다.
유니티에서 GameObject->Create Empty메뉴를 이용해서 빈 게임오브젝트를 생성합니다. 생성된 게임오브젝트의 이름을 ObjectPool로 지정합니다.
이렇게 생성된 ObjectPool게임오브젝트에 ObjectPool스크립트를 컴포넌트로 추가합니다.

objectpool creation

컴포넌트가 추가되었으면, ObjectPool의 Size값에 1을 입력하고 엔터를 누릅니다. 그러면 아래 그림과 같이 입력할 수 있는 정보들이 나타나게 됩니다.
각 항목의 정보를 아래 내용을 참고해서 설정합니다.

  • Pool Item Name : Bullet 입력 -> BulletMove스크립트의 poolItemName 변수에 설정한 이름과 동일해야 함
  • Prefab : 위에서 만든 Bullet 프리팹을 드래그해서 설정
  • Pool Count : 처음에 총알 프리팹을 생성할 숫자 -> 넉넉히 20 입력
  • Pool List : 그냥 두면 됨 -> 나중에 실행할 때 자동으로 관리되는 리스트

setting parameters

이렇게 하면 오브젝트 풀이 완성됩니다. ObjectPool.Instance.PushToPool함수나 ObjectPool.Instance.PopFromPool함수를 이용해서 객체를 반환하고 요청하는 것이 가능해졌습니다.

 

플레이어 만들기

총알 프리팹과 이를 관리하는 오브젝트 풀까지 준비되었습니다. 이제 총알을 발사시키는 플레이어를 만들겠습니다.

유니티에서 GameObject->3D Object->Capsule메뉴를 선택해서 캡슐 게임오브젝트를 생성합니다. 생성된 게임오브젝트의 이름은 Player로 지정합니다.
화면에 잘 보이도록 하기 위해서 플레이어의 위치를 (0, -2, 0)으로 입력합니다.

게임오브젝트 설정이 완료되었으면 총알 발사에 필요한 스크립트를 만들어 보겠습니다.

using UnityEngine;
using System.Collections.Generic;

public class Player : MonoBehaviour
{
    public string bulletName = "Bullet";

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Shoot();
        }
    }

    void Shoot()
    {
        GameObject bullet = ObjectPool.Instance.PopFromPool(bulletName);
        bullet.transform.position = transform.position + transform.up;
        bullet.SetActive(true);
    }
}

먼저 bulletName 변수는 오브젝트 풀에서 관리하는 총알 프리팹의 오브젝트 이름입니다. 이 값은 위에서 입력한 값과 동일해야합니다.

Update함수에서 마우스 왼쪽 클릭 입력이 발생했는 지 확인하고 입력이 있으면, Shoot함수를 호출합니다.

Shoot함수에서는 ObjectPool.Instance.PopFromPool함수를 호출해서 오브젝트 풀로부터 총알 객체를 요청합니다. 이어서 오브젝트 풀로부터 받은 총알의 위치를 플레이어의 위치보다 조금 높게 설정하고, 게임오브젝트를 활성화해서 총알이 화면에 보이면서 위로 이동할 수 있도록 합니다.

스크립트 작성이 완료되면, 아래 그림과 같이 Player 게임오브젝트에 컴포넌트로 추가합니다.

add player component

여기까지 문제없이 진행되었으면, 필요한 모든 작업이 모두 완료되었습니다. 이제 유니티를 실행해서 마우스 왼쪽 버튼을 마구 클릭합니다.

firing bullet

총알이 위로 발사되는 것을 볼 수 있습니다.
실행하면서 하이어라키에 있는 ObjectPool게임오브젝트를 선택해서 PoolList의 상태를보면, 마우스 왼쪽 버튼이 마구 눌리면서 총알이 발사될 떄는 Size가 줄어들었다가 마우스 클릭을 멈추면 Size가 다시 늘어나는 것을 확인할 수 있습니다.

object pool in action

이게 바로 사용할 때는 오브젝트 풀에서 꺼내 썼다가, 일정 시간이 지나서 총알의 수명이 다하면 다시 오브젝트 풀에 반환하기 때문에 Size가 줄었다 늘었다 하면서 동작하는 것입니다.

오브젝트 풀을 구성하는 방법은 객체를 그때 그때 생성/해제하는 방법보다 조금 귀찮은 방법일 수 있지만, 메모리 관리 측면에서는 가비지 컬렉션을 피할 수 있기 때문에 매우 좋습니다.

따라서 자주 사용해야 하는 객체의 경우 오브젝트 풀을 이용해서 관리하시기 바랍니다.

전체 예제 유니티 프로젝트는 아래 링크에서 다운로드 받을 수 있습니다. 강좌를 진행하다가 문제가 생겼거나, 코드를 참고하고 싶은 분들은 다운로드 받아서 확인해 보시기 바랍니다.

오브젝트 풀 유니티 예제 다운로드

 

내용 끝까지 읽어주셔서 감사합니다.

배너 클릭은 저에게 많은 힘이 됩니다.

감사합니다 🙂

RonnieJ

프리랜서 IT강사로 활동하고 있습니다. 게임 개발, 웹 개발, 1인 기업, 독서, 책쓰기에 관심이 많습니다.

30 Responses

  1. 전준호 댓글:

    총알이 적을 발견하면 적방향으로 발사할려면
    어떻게 하죠?넓은 아량으로 부탁드립니다.

    • RonnieJ 댓글:

      방법은 여러가지가 있을 것 같은데, 생각나는 접근 방법을 말씀 드리겠습니다.
      적 위치에서 현재 총알 위치를 빼면(벡터 빼기) 총알에서 적으로 향하는 방향 벡터를 얻을 수 있습니다.
      이걸 전체 거리로 두고, 총알 속도를 적용해서 적 방향으로 이동시켜주면 될 것 같습니다.

  2. 닝구 댓글:

    꿀 팁 감사합니다! 씨샵 초보인데 덕분에 쉽게 구현했습니다.

    궁금한 점이 있는데요, pool item name이 꼭 필요한 이유가 있나요?
    prefab을 갖고 있으니 그걸로 풀을 검색해서 동작하게 끔 만들 순 없는지 궁금합니다.

    그리고 미리 풀에 프리팹을 지정하지 않고 새로운 종류의 프리팹이 사용되면 그때 그때 풀이 생성되게 하고 싶은데요, 이런건 어떻게 구현하면 좋을지 궁금합니다.
    (PopFromPool의 리턴이 null일때 풀 리스트를 add로 늘린후에 프리팹을 넣어주는 식으로 해보려 했는데, 널 레퍼런스 오류가 뜨네요)

    • RonnieJ 댓글:

      제가 설명해놓은 오브젝트 풀은 기초적인 버전이고, 오브젝트 풀에서 오브젝트를 검색할 때 이름을 사용합니다.
      필요한 오브젝트를 얻을 때(이름 검색), 사용한 오브젝트를 다시 풀에 넣을 때(이름 검색) 이렇게 두 경우에 사용합니다.
      보통은 이름< ->ID(숫자) 이렇게 테이블을 만들어서 특정 프리팹정보를 오브젝트 이름이 아니라 ID(숫자)로 검색을 하도록 하는 것이 성능면에서는 더 좋습니다.

      프리팹 정보 자체를 비교해서 구현하는 방법도 가능은 할 것 같습니다. 하지만 직접 좀 더 시도해 보시는 것이 좋을 것 같네요 🙂
      PopFromPool 메소드를 보면 GetPoolItem 메소드를 안에서 호출하는 것을 볼 수 있습니다.
      GetPoolItem에서 특정 오브젝트를 검색해보고 없으면 null을 리턴하는데, 이 부분을 작업해보시면 원하시는 기능을 구현할 수 있을 것 같습니다.

      그리고 이름 검색이 아니라 해당 프리팹 정보를 검색해서 오브젝트를 얻고/다시 넣는 기능을 하도록 전체적으로 시스템을 업데이트하셔야 할 것 같습니다.

      블로그 방문 감사합니다. 자주 들러주시고, 배너도 눌러주세요 🙂

  3. 유병영 댓글:

    안녕하세요, 좋은 강의글 써주셔서 감사합니다^^

    만약에 동일한 총알 GamgeObject를 Sprite(겉모습) 만 달리해서 두개의 프리펩으로 구현했을때
    하나의 오브젝트풀에 두개를 다 넣어서 사용하는것이 가능할까요?

    • RonnieJ 댓글:

      두 프리팹을 서로 다른 총알로 구분해서 사용하신다면 오브젝트 풀에 넣어서 사용하는 것이 가능할 것 같습니다.
      물론 Sprite는 코드에서 사용할 때 변경이 가능하기 때문에 실행 중에 조립하는 형태로도 사용한다면 하나의 프리팹으로 사용할 때 겉모습만 바꿔서 사용하는 것도 가능하겠네요 ㅎㅎ

  4. 유병영 댓글:

    추가 질문 하나 더 드립니다.
    만약 디펜스게임 구현시 10종류의 적이 10마리씩 나온다고 했을때(총 100마리), 한화면에 100마리가 등장할 수있다면, 결국에는 100마리 모두를 미리 오브젝트풀에 넣어놓고 스테이지를 시작하는 방법이 옳은 방법일까요?

    미리 답변 감사드립니다.^^

    • RonnieJ 댓글:

      상황에 따라 다르겠지만, 스테이지에서 확실하게 사용되는 리소스라면 사전에 오브젝트 풀에 넣고 시작하는 것이 좋아보입니다.
      실행 중에 Instantiate로 메모리를 할당하는 작업은 시스템에 부하를 가중하기 때문에 최악의 경우 화면이 끊기는 경우가 발생할 수 있는데, 로딩 화면이나 스테이지 시작 전에 미리 오브젝트 풀에 넣고 시작하면 이런 현상을 없앨 수 있습니다 🙂

  5. 원준 댓글:

    안녕하세요 RonnieJ님 정말 좋은강의 감사히 보고있습니다.

    저는 C#초보 개발자고, 3D슈팅게임을 구현중인데요. 한 자리에서 사방(앞뒤좌우)으로 나오는 적에게 카메라가 응시하는 방향으로 미사일을 발사하려고 카메라의 각도를 미사일에 달고 발사를 시켰는데, 문제는 처음에 미사일들이 인스턴스 됐을 때(배열을 1회 순환)는 카메라가 응시하는 방향으로 발사가 됐다가 두번째로 poolList배열을 돌때부터는 카메라의 방향이 아닌 처음에 쐈던 그 자리 그대로 발사가 되네요.

    PoolList로 다시 PushToPool될 때 rotation값을 Quaterion.identity로 초기화 시켜주어도 문제가 해결되지 않던데 어떻게 해야할지 조언구합니다.

    • RonnieJ 댓글:

      안녕하세요~

      오브젝트 풀을 사용할 때 까다로운 부분 중 하나가 사용했던 오브젝트의 값을 다시 초기화 하는 것인데,
      제가 올린 강좌는 간단한 예제를 위주로 하기 때문에 그런 부분에 대한 언급이 없었던 것 같네요.

      먼저 PushToPool을 이용해서 사용한 오브젝트를 다시 오브젝트 풀에 넣을 때는 오브젝트를 사용할 때 변경될 만한 값을 다시 초기화 해주는 것이 좋습니다.
      미사일의 경우에는 위치, 회전, 스케일, 발사 방향, 발사 속도 등의 정보가 사용될 수 있으니 이런 정보를 초기화하고 오브젝트를 꺼두는 것이 좋습니다.
      그리고 PopFromPool을 이용해서 오브젝트를 꺼내 사용할 때 미사일에 설정해야 하는 값을 설정해서 사용해 주면 됩니다.

      따라서 오브젝트를 PopFromPool을 이용해서 꺼낸 다음 마사일의 발사 시작위치, 회전 값, 발사 진행 방향, 미사일 속도, 데미지 등의 정보를 설정해주면 될 것 같습니다.

      블로그 방문 감사드립니다. 방문하셨을 때 배너 광고도 클릭해주시면 큰 힘이됩니다! ㅎㅎ

      • 원준 댓글:

        아… 제가 생각하지 못했던 요소들이 몇 개 있었네요….
        친절한 답변 너무너무 감사드립니다!!

        저는 미사일의 자식오브젝트로 Trail Renderer나 Particle등을 입혀놨었는데
        혹시 부모 미사일오브젝트의 값(회전, 위치 등)을 초기화 시켜줬는데도 제대로 실행되지 않는다면
        자식오브젝트들도 영향을 끼칠 수 있을까요???

        • RonnieJ 댓글:

          음..어떤 상황인지 정확히 보지 못해서 알 수는 없지만,
          Trail Renderer와 Particle 모두 부모 트랜스폼에 영향을 받아 동작하도록 되어 있습니다.
          따라서 미사일 오브젝트가 부모 트랜스폼인 경우, 미사일이 발사되기 직전에 필요한 값들이 설정되면 문제 없을 것 같습니다.

          물론 부모 트랜스폼이 바뀌면 자식 트랜스폼에 영향을 줍니다.
          유니티는 자식 트랜스폼은 항상 부모 트랜스폼에 상대적인 값(로컬 좌표)으로 설정되어 동작하도록 설계되어 있습니다.

  6. deck X 댓글:

    안녕하세요 좋은 정보 주셔서 감사드립니다 🙂
    다름이 아니라 특정 오브젝트를 풀링(Count = 10으로 설정)에서 꺼내서 사용한 뒤에
    콜라이더에 부딪힌 후 사라지게했는데
    Pool List size에서 10개에서 9개로 줄어들고 다시 원래 count로 돌아가지 않더라구요
    혹시 제가 콜라이더에 부딪히고 사라질때 코딩을 잘못했는지(해당오브젝트.SetActive(false);)
    혹은 그게 아니라면 Pool List의 Size를 ++1을 하는 작업을 해줘야하는지 궁금합니다!

    • RonnieJ 댓글:

      안녕하세요~ 블로그 방문 감사합니다!
      풀 오브젝트를 다 사용하고 나서는 SetActive(false);가 아니라 PushToPool(“풀 아이템 이름”, gameObject);를 이용해서 오브젝트 풀에 다시 넣어줘야 합니다.
      (SetActive 메소드는 단순히 게임 오브젝트를 활성화/비활성화 하는데 사용합니다.)
      풀 사이즈는 부족하면 늘도록 제작 해뒀으니 예제 시리즈를 다시한번 읽어보시면 좋을 것 같습니다 🙂

      가끔 오셔서 배너도 클릭해주시면 블로그 운영에 큰 힘이 됩니다!
      감사합니다.

  7. 병만 댓글:

    안녕하세요 좋은 공부가 되었습니다.
    궁금한점이 있는데요 오브젝트풀로 text출력하려하는데
    오브젝트풀에 넣으면 계속 초기화되서 text글이 안뜨는데 어떻게 하면 좋을까요…ㅠㅠ
    아직 공부하는 초보자라 어렵네요..

    • RonnieJ 댓글:

      먹고사는데 바쁘다보니 답변이 너무 늦었네요 ㅠ
      오브젝트 풀에 넣어놓고나서 다시 불러서 사용할 때 필요한 Text를 다시 설정해주면 될 것 같습니다 🙂

  8. 이대호 댓글:

    포스팅 잘 보았습니다…
    답변을 빨리 주실지는 모르겠지만 팁을 얻고싶어서 질문 드립니다…
    현재 짜두신 코드는 총알이 위로 나가는데요
    이걸 정면으로 나가게하고 총알을 발사하는 캐릭터가 바라보고 있는 방향으로 나가게 하고싶은데…
    제가 혼자 해보려 했지만 유니티에 아직 익숙치않아서 그런지 함수를 뭘 써서 해야될지도 모르겠고…
    막히더라구요…..;; 그래서 어떤 함수를 사용하거나 어떤식으로 해야할지 팁을 좀 얻고싶습니다;
    대력 캐릭터의 로테이션을 받아와서 총알이 같은 방향으로 나가게한다…이런거 생각했는데… 잘 안되네요 ㅠㅠ

    • RonnieJ 댓글:

      안녕하세요~
      Bullet에서 가지고 있는 스크립트에 보면 아마 transform.up 벡터를 사용할텐데
      이 부분을 transform.forward 벡터를 사용하도록 바꾸면 캐릭터가 바라보는 방향으로 발사되게 만들 수 있습니다~

      블로그 방문 감사드립니다.
      자주 들러주시고 배너도 가끔 눌러주세요
      감사합니다 🙂

  9. 이대호 댓글:

    공부하는데 도움이 되었습니다.
    그런데 팁을 좀 받고 싶은게있는데요
    총알을 위로 발사하는게 아닌 캐릭터가 바라보는 방향으로 앞으로 쭉 나가는걸 하고싶어서요…
    제가 유니티가 익숙치않고 함수도 잘 몰라서 혼자 이것저것 해보려는데 잘 안되어서요…;
    이걸 어떤함수를 써야되는지 어떤식으로 처리를 해야될지 팁을 좀 받고싶습니다…ㅠㅠ;

    • RonnieJ 댓글:

      안녕하세요~
      문의주신 내용은 총알 발사 시킬때 캐릭터에서 transform.forward를 이용하면 캐릭터가 바라보는 방향으로 발사시킬 수 있습니다.

      블로그 방문 감사드립니다~
      자주 들러주세요 🙂

  10. 리버티 댓글:

    오브젝트 풀링 공부중이었는데 잘봤습니다
    2D 횡스크롤 게임을 제작중인데 플레이어 스크립트를 찾아서 OnEnable함수로 localsacle이 0보다 작을때 총알방향이 좌우로 가게끔 코드를 짰는데
    가끔가다 반대방향으로 발사를 하더군요;;
    혹시 다른 좋은방법이 있는지 궁금합니다ㅠㅠ

    • RonnieJ 댓글:

      그럼 OnEnable 말고 PopFromPool() 함수 호출하면서 총알 받아올 때(예를 들어 Shoot 함수) localscale 값을 확인해서 0보다 작을 때 방향을 반대로 나가도록 해보세요~

  11. like a shen 댓글:

    좋은 글 감사합니다.
    오브젝트 폴링을 이해하는데 많은 도움이 되었습니다.
    종종 놀러와서 글도 읽고 광고도 보고 갈게요.

  12. 아세 댓글:

    ObjectPool이 컴포넌트로 추가될때 The script needs to be derive from MonoBehaviour라는 에러가 뜨며 추가가 안돼는데 어떻게 해야하나요ㅜㅠ

    • RonnieJ 댓글:

      혹시 Singleton 클래스는 다른 예제 보시고 작성하셨나요?
      안하신거라면 본문에 링크 있으니 가셔서 다시 작성하시면 됩니다.
      오류는 ObjectPool 클래스가 Monobehaviour를 상속 받지 않아서 발생하는 문제인데, 싱글톤 클래스가 문제인 것 같네요.
      확인해보세요 🙂

  13. 초보 댓글:

    안녕하세요 . 정리된 글 잘 보았습니다.

    제가 따라하던 중 3개의 프리팹을 만들어서 5개씩 오브젝트풀링을 했는데

    랜덤으로 불러올 순 없나 궁금합니다.

    popfrompool(오브젝트네임)을 하면 해당 이름을 가진 것이 액티브 트루가 되는데

    저는 프리팹된 3개중에 하나를 랜덤으로 나오게 하고 싶어서요 ㅠ

    • RonnieJ 댓글:

      랜덤으로 부른다는 것이 무기의 종류를 다르게 하고 싶다는 건가요?
      이게 맞다면 종류가 다른 풀링 오브젝트를 3개 만들고 발사할 때 랜덤으로 이름을 지정하게 하면 될 것 같습니다.
      예를 들어 “BulletNormal”, “BulletCube”, “BulletSphere” 이렇게 세가지가 있다고 가정하면,
      총알을 발사하기 전에 Random으로 0~3 까지 숫자를 고른다음
      0이면 “BulletNormal”를 선택하고 1이면 “BulletCube”를 선택하고 2이면 “BulletSphere”를 이름으로 선택하게 해서
      총알을 발사시키면 될 것 같습니다 🙂

      안녕하세요~블로그 방문 감사합니다.
      배너도 클릭해주세요~ 큰 힘이 됩니다 🙂

  14. ㅁㅁ 댓글:

    댓글이 왜 안남ㄱㅕ지지

    • RonnieJ 댓글:

      참고로 스팸 떄문에 댓글을 일일이 승인하고 있습니다.
      물론 아직까지 스팸 말고는 댓글 승인을 하지 않은 적은 없어요 ㅎㅎ

      블로그 방문 감사합니다 🙂

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다