게시판 - 댓글 기능 만들기 4 (댓글 수 표시)

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 게시판 만들기(고급) / 게시판 - 댓글 기능 만들기 3 (대댓글 기능)에서 이어집니다.

board.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard 39225f3
git reset --soft 1212b2e
npm install
atom .

board.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 board.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/board.git
cd board
git reset --hard 39225f3
git reset --soft 1212b2e
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/board/tree/39225f3b93a24c4544ba8225d626a2e5b6ec0296


게시물(post)의 index 페이지에 댓글(comment)의 수를 표시해 봅시다. 댓글이 있는 경우 게시물 제목 옆에 ()안에 댓글 수가 표시됩니다.

현재 우리는 post와 comment의 관계를 comment model에 저장하고 있습니다. comment의 post 항목에 post id를 저장하여 해당 comment가 어느 post에 연결되어 있는지를 찾을 수 있죠.

그래서 댓글을 읽어올 때는 comment모델을 이용해서 해당 post의 id를 가진 comment들을 찾아서 읽어 옵니다.

게시물의 index페이지에 댓글수를 나타내려면, 각 게시물에 연관된 댓글들을 찾아야 하는데, view에 보여줄 post들을 모두 찾고, 각 comment 모델을 이용해서 각각의 글에 대한 댓글들을 찾아야 할까요? 물론 이렇게 코드를 작성해도 작동은 합니다. 다만 DB와 서버간 자료전달은 그 횟수가 적으면 적을수록 속도가 빠른데 이러한 방식은 비효율적입니다.

한번의 DB요청으로 post의 리스트와 각각 post에 연관된 댓글들을 모두 찾을 수 있는 방법을 알아봅시다.

MongoDB - Aggregation

혹시 SQL계열의 DB를 사용해보신 분들은 위 문제를 join을 써서 아주 쉽게 해결할 수 있습니다. Join은 SQL DB에서 두개의 테이블을 어떠한 키로 연관지어 하나로 합치는 명령어입니다. mongo DB에도 이러한 기능이 있을 까요?

지금까지 우리는 강의에서 두개의 model을 연결할 때 populate함수를 사용했습니다. 이때 문제가 하나 있습니다. comment에는 post의 id를 가진 항목(post)가 존재하기 때문에 comment에 post를 populate할 수 있지만, 반대로 post에는 comment의 정보가 전혀 없기 때문에 post로부터 comment를 populate할 수 없습니다.

populate함수는 mongo DB에서 제공하는 함수가 아니라, mongoDB library인 mongoose에서 제공하는 함수입니다. 현재까지 mongoose를 이용해서 해당 model에 존재하지 않는 데이터를 이용하여 다른 collection을 읽어오는 함수는 존재하지 않습니다.

대신 우리는 mongo DB의 aggregation을 이용해 SQL의 query에 해당하는 복잡한 데이터의 처리를 할 수 있습니다.

collection_이름.aggregate([ query_오브젝트1, query_오브젝트2, query_오브젝트3...  ])

query_오브젝트로 하나의 명령어가 전달됩니다. 위 처럼 여러개의 명령어를 배열로 전달하여 데이터를 조작하고 가공할 수 있습니다. 바로 게시판 코드에 적용된 코드를 살펴봅시다.

폴더 구조

총 4개의 파일이 업데이트 되었는데요, 중요도만 보면 post.js가 99%, index.ejs가 1%, master.css, script.js는 각각 0%입니다.

코드 - js

// routes/post.js

...

// Index
router.get('/', async function(req, res){
  ...

  if(searchQuery) {
    var count = await Post.countDocuments(searchQuery);
    maxPage = Math.ceil(count/limit);
    posts = await Post.aggregate([ // 1
      { $match: searchQuery }, // 2
      { $lookup: { // 3
          from: 'users',
          localField: 'author',
          foreignField: '_id',
          as: 'author'
      } },
      { $unwind: '$author' }, // 4
      { $sort : { createdAt: -1 } }, // 5
      { $skip: skip }, // 6
      { $limit: limit }, // 7
      { $lookup: { // 8
          from: 'comments',
          localField: '_id',
          foreignField: 'post',
          as: 'comments'
      } },
      { $project: { // 9
          title: 1,
          author: {
            username: 1,
          },
          createdAt: 1,
          commentCount: { $size: '$comments'} // 10
      } },
    ]).exec();
  }

  ...

1. mongoose에서 모델.aggregate함수로 모델에 대한 aggregation을 mongodb로 전달할 수 있습니다. 함수에 전달되는 배열의 오브젝트 형태는 mongoDB에서 사용되는 오브젝트와 정확히 일치하므로, 여기서부터는 mongoDB의 aggregation 문서(https://docs.mongodb.com/manual/aggregation)를 참고할 수 있습니다.

2. $match 오브젝트(https://docs.mongodb.com/manual/reference/operator/aggregation/match)는 모델.find함수와 동일한 역할을 합니다. 즉,

Post.find(searchQuery).exec();

Post.aggregate([{ $match: searchQuery }]).exec();

는 정확히 같은 일을 합니다. 그럼 aggregation을 왜 써야 하는가, aggregation에서만 할 수 있는 복잡한 일들이 있습니다. 이 때문에 간단한 일을 할 때는 그냥 일반 모델.함수의 형태로 사용하고, aggregation으로만 할 수 있는 일이 개입되면 이처럼 aggregation으로 고쳐주면 되겠습니다.

3. $lookup 오브젝트(https://docs.mongodb.com/manual/reference/operator/aggregation/lookup)는 한 collection과 다른 collection을 이어주는 역할을 합니다. 네가지 하위 항목을 필수로 갖습니다.

  • from: 다른 collection의 이름을 적습니다.
  • localField: 현재 collection의 항목을 적습니다.
  • foreignField: 다른 collection의 항목을 적습니다.
  • as: 다른 collection을 담을 항목의 이름을 적습니다. 이 항목의 이름으로 다른 collection의 데이터가 생성됩니다.

우리가 만든 오브젝트를 살펴봅시다.

{ $lookup: {
    from: 'users',
    localField: 'author',
    foreignField: '_id',
    as: 'author'
} },

user collection에서 (mongoose는 collection 이름을 항상 영어 복수형으로 생성합니다. 그러므로 user가 아닌 users가 되어야 합니다) post.author의 값을 가지는 user._id들을 모두 찾아 post.author로 덮어쓰고 있습니다. 이때 post.author는 배열의 값을 가집니다. 만약 하나도 없다면, 빈배열을 갖습니다.

4. $unwind 오브젝트(https://docs.mongodb.com/manual/reference/operator/aggregation/unwind)는 배열을 flat하게 풀어주는 역할을 합니다.

예를 들어

[
  { _id:1, name:"john", classes:["math", "history", "art"] }
]

이러한 document가 testDocuments 콜렉션에 있다고 가정하면,

TestDocument.aggregate([{ $unwind: '$classes'}])

위 명령어를 사용할 수 있습니다. $classes처럼 aggregation에서 값으로 '$'가 사용되면, 현재 document의 항목을 나타냅니다. 즉, testDocument의 classes항목을 값으로 전달하기 위해 '$classes'를 사용한 것입니다.

[
  { _id:1, name:"john", classes:"math" }, 
  { _id:1, name:"john", classes:"history" }, 
  { _id:1, name:"john", classes:"art" }
]

이러한 값을 가져 올 수 있습니다.(DB안의 값이 바뀌진 않습니다).

3번에서 $lookup을 사용해 user를 author로 가져왔는데, 말했던 것처럼 배열로 가져옵니다. 이를 flat하게 풀어주기 위해 사용하였습니다. 물론 post.author는 user._id를 갖기 때문에 하나 이상의 값을 가질 수 없으므로 전체 검색결과의 수가 늘어나진 않습니다.

5. $sort 오브젝트(https://docs.mongodb.com/manual/reference/operator/aggregation/sort)는 모델.sort함수와 동일한 역할을 합니다. 다만 '-createdAt'과 같은 형태는 사용할 수 없고, { createdAt: -1 }의 형태로 사용해야 합니다. (모델.sort은 두가지 형태를 모두 사용할 수 있습니다.)

6. $skip 오브젝트(https://docs.mongodb.com/manual/reference/operator/aggregation/skip)는 모델.skip함수와 동일한 역할을 합니다.

7. $limit 오브젝트(https://docs.mongodb.com/manual/reference/operator/aggregation/limit)는 모델.limit함수와 동일한 역할을 합니다.

7번까지는 정확히 지난번과 같은 코드를 aggregation으로 변경한 것입니다.

변경 전 변경 후
posts = await Post.find(searchQuery)
  .populate('author')
  .sort('-createdAt')
  .skip(skip)
  .limit(limit)
  .exec();
posts = await Post.aggregate([
      { $match: searchQuery },
      { $lookup: {
          from: 'users',
          localField: 'author',
          foreignField: '_id',
          as: 'author'
      } },
      { $unwind: '$author' },
      { $sort : { createdAt: -1 } },
      { $skip: skip },
      { $limit: limit },
    ]).exec();

여기에 아래와 같이 두개의 aggregation 오브젝트들이 추가되었습니다.

      { $lookup: { // 8
          from: 'comments',
          localField: '_id',
          foreignField: 'post',
          as: 'comments'
      } },
      { $project: { // 9
          title: 1,
          author: {
            username: 1,
          },
          createdAt: 1,
          commentCount: { $size: '$comments'}
      } },

8. 또다시 $lookup을 사용해서 post._id와 comment.post를 연결합니다. 하나의 post에 여러개의 comments가 생길 수 있으므로 이번에는 $unwind를 사용하지 않습니다.

9. $project 오브젝트(https://docs.mongodb.com/manual/reference/operator/aggregation/project)는 데이터를 원하는 형태로 가공하기 위해 사용됩니다. $project:바로 다음에 원하는 schema를 넣어주면 됩니다. 이때 1은 보여주기를 원하는 항목을 나타냅니다. 다만 _id는 반드시 표시되고 숨길 수 없습니다. 즉 title: 1은 '데이터의 title항목을 보여줄 것'이라는 뜻입니다.

commentCount라는 항목을 새로 생성하고 있는데, 여기에는 $size (https://docs.mongodb.com/manual/reference/operator/aggregation/size)를 사용해서 comments의 길이를 가져옵니다.

aggregation을 통해 post에 commentCount항목을 생성하고 관련 comments의 길이를 갖는 코드를 작성해 보았습니다. 나머지는 이 코드를 어떻게 화면에 보여줄지를 위한 코드이고, 위까지의 내용이 중요한 내용이니 잘 이해해 보도록 합시다. 특히나 $project는 지금 사용한건 아주 기본적인 내용이고 활용방법이 아주 다양합니다.

코드 - ejs

<!-- views/posts/index.ejs-->

          ...
          <% posts.forEach(function(post) { %>
            <tr>
              <td>
     <!-- 1 --> <a href="/posts/<%= post._id %><%= getPostQueryString() %>" class="title-container">
                  <div data-search-highlight="title" class="title-ellipsis ellipsis float-left">
                    <span class="title-text"><%= post.title %></span>
                  </div>
       <!-- 2 --> <% if(post.commentCount){ %>
                    <small class="title-comments d-inline-block pl-2">(<%= post.commentCount %>)</small>
                  <% } %>
                </a>
              </td>
              ...

1. ellipsis(텍스트가 너무 길 경우 해당 범위 밖의 텍스트를 '...'로 표시하는 것)를 구현하기 위해 title-text, title-comments등의 CSS class들이 추가 되었습니다.

2. 댓글의 수는 post route를 통해서 이미 post.commentCount에 저장되어 있으므로 어떻게 보여줄지만 고민하면됩니다. 

코드 - css

// public/css/master.css

...
.board-table .author,
.board-table .date {
  width: 100px;
}
.board-table .title-container:hover .title-comments, /* 1 */
.board-table .title-container:hover .title-text
{
  text-decoration: underline;
}

...

a 태그에 하위 태그가 들어가서 게시물 제목에 마우스오버시 밑줄이 더이상 생기지 않길래 css로 해결하였습니다.

코드 - client js

// public/js/script.js

...

$(function(){
  function resetTitleEllipsisWidth(){
    $('.board-table .title-text').each(function(i,e){
      var $text = $(e);
      var $ellipsis = $(e).closest('.title-ellipsis');
      var $comment = $(e).closest('.title-container').find('.title-comments');

      var textWidth = $text.width();
      var ellipsisWidth = $ellipsis.outerWidth();
      var commentWidth = $comment.outerWidth();
      var padding = 1;

      if(ellipsisWidth <= (textWidth+commentWidth+padding)){
        $ellipsis.width(ellipsisWidth-(commentWidth+padding));
      }
      else {
        $ellipsis.width(textWidth+padding);
      }
    });
  }
  $(window).resize(function(){
    $('.board-table .title-ellipsis').css('width','');
    resetTitleEllipsisWidth();
  });
  resetTitleEllipsisWidth();
});

이 코드는 이번 강의의 내용과는 정말 아무상관도 없는 내용인데요, 게시물의 제목이 아주 긴 경우 댓글의 수를 '...' 다음에 표시해 주기 위한 코드입니다. 정말 신경안쓰셔도 괜찮아요.

마치며...

Mongo DB의 aggregation은 그 한 주제만 가지고도 책한권이 나올 만큼 많은 내용을 담고 있습니다. 이 강의의 목적은 이번 강의에서 소개했던 기능들에 대한 맛보기 정도로, 이러한 것들이 있다는 것을 알려드리는 것입니다. 보통 sql로 만드는 복잡한 query는 mongo DB의 aggregation으로 동일한 일을 할 수 있습니다. 이러한 점을 기억하고 나중에 필요할 때 찾아서 공부할 수 있도록 합시다.

Aggregation 관련 공식 문서 링크:

댓글

아띠 엔터테인먼트 2020.02.12
구글로그인은 internal server error가 나는군욤...
이안 선생님 정말 감사하게 잘 보고 배우며 응용하고 사용하고 있습니다만, 기존에 엑셀파일을 가지고 excel -> json 변형해서 mongo atlas 서버에 mongoimport로 저장을했습니다.
그래서 그걸 불러와서 게시판형식으로 이용하려고 했으나... post부분을 이용해서 다른 게시판이나 이런것들은 다른 응용은 전부 잘 되는데
json으로 mongoimport 한 부분은 삼일째 고생중이나 불러와 지지가 않네요;;
혹시 좋은 방법 아실까요?
I
Ian H 2020.02.12
@아띠 엔터테인먼트,
안녕하세요 구글 로그인 오류 제보주셔서 감사합니다. 현재는 수정하여 잘 작동합니다. 감사합니다!
atlas에 로그인하면 Clusters -> SANDBOX -> COLLECTIONS 에서 DB상의 데이터들을 볼 수 있는데, 거기서 data들이 제대로 import되었는지 먼저 확인해 보시구요, 다음으로 import된 데이터와 기존의 데이터를 한번 비교해 보세요.
P
Peter Kim 2020.02.12
@Ian H,
한번도 몽구스스키마를 통해 데이터 입력없이 import로만 데이터를 입력해서 였을까요?
Author항도 ObjectId 하나하나 입력해서 잘 들어갔는데...
다시 한번 해봐야겠어요.
I
Ian H 2020.02.12
@Peter Kim,
몽구스 스키마를 통해 데이터를 입력하진 않았지만 몽구스 스키마는 있으시죠? 없다면 만들어야 합니다.
P
Peter Kim 2020.02.14
@Ian H,
ㅎㅎ 스승님 이번 문제는 해결했습니다. 추가로 하나 여쭤보고 싶은게.. db.find() 항에서 멋지게 작성해주신 searchQuery 와 searchText를 이용하고있는데요.
mongoose schema에서 제가 datatype을 Boolean으로 만든거를 검색이나 아니면 url로 해서 Boolean에서 true 나 false로 구분해서 목록을 불러오게 하고싶은데, Boolean을 searchQuery 로 만들거나 text+boolean 으로 검색해서 data를 구분하려면 어떻게 해야할까요?
물론 db.find() 에서 조건을 여러가지로 page를 따로 만들어서 해도 될것같긴한데(사실도전해도 될지안될지.. page도 급수적으로 늘어날것같고욤..) 멋진 searchQuery를 만들어 놓으신 내에서 하게 된다면 어떻게 될지 여쭙니다. regex와 regexp는 어렵네여...
(질문료는 어떻게 해야할지 ㅠㅠ)
I
Ian H 2020.02.14
@Peter Kim,
routes/post.js의 createSearchQuery 함수 속 코드를 수정하시면 되는데요, 만드시려는 것이 현재 기능에서 boolean query를 추가하고, 현재 searchText와 새로운 boolean 조건 둘 다 만족되는 게시물만 검색하려는 것이 맞나요?
createSearchQuery 함수 의 return 직전에 boolean query가 있는 경우,  최종 searchQuery를 {$and:[{$or:postQueries},새조건]} 으로 만드는 코드를 추가해 보세요.
I
Ian H 2020.02.14
질문료는 블로그를 다른 사람들에게도 많이 알려주세요^^ 감사합니다!
J
Jake Lyu 2020.05.03
안녕하세요 고급으로 넘어오면서 기능을 구현하기 보다는 카피하기 바쁘네요 ㅠㅠ 기본이 없다보니 이해가 좀 어렵네요..    현재 구현해 놓은 대댓글을 보면 들여쓰기가 눈에 잘 띄지 않는데 조금 더 들여쓰기 하면서 " ㄴ> " 이런 식의 기호로 대댓글임을 표시하려면 어디를 만져야하는 지 찾지를 못하겠습니다.  
예)   댓글    : nodejs is  대댓글 :    ㄴ> mogodb.. 
J
Jake Lyu 2020.05.03
공부방향에 대해서 조언 구할 수 있을까요?
이안님 강의 보면서 제 웹사이트를 만드는데 적용하고 있습니다.  문제는 고급 전까지 충분히 이해하며 스스로 수정할 수 있을정도로 따라가고 있었는데, 고급 어디선가부터 이해를 거의 하지 못한채 카피하는 수준으로 따라가고 있습니다. 이렇게라도 결과물을 만들어 내는 것이 중요한가요? 아니면 udemy에서 mongodb 및 JavaScript 강의를 더 듣고 충분히 이해하면서 따라가는게 맞을까요?  
원래 무엇인가를 제작할때 프로그래머분들은 충분히 본인이 이해한 코드를 작성하는 것인지? 아니면 누군가 작성해 놓은것을 가져와서 고쳐 쓰는 것인지 궁금합니다. 
스스로 고쳐쓸 수 있는 웹페이지 만들어 보겠다고 공부를 시작해봤는데, 어디선가 에러가 발생하고 이해하기 어려운 부분이 생길때마다 WordPress로 쉽게쉽게 갈껄?하는 생각이 수도 없이 스쳐갑니다 ㅋㅋ 생각했던것 보다 공부해야할 것이 공부할 수록 끝도 없다는 것을 깨닫고나니 도무지 무엇이 맞는 것인지 잘 모르겠습니다.  (프로그래밍과 전혀 무관한 일은 하고 있고, 매일 출근 전 퇴근 후 짬짬이 2~3시간 공부하고 있습니다.)  
I
Ian H 2020.05.04
@Jake Lyu,
프로그래밍에서 배울것이 수도 없이 많은 것은 사실이지만, 프로그래머가 그 모든 것들을 기억해야 하는 것은 아닙니다. 저도 인터넷 없이 프로그램 만들라고 하면 다 못만듭니다. 인터넷 없이 이 게시판 코드를 처음부터 작성하라고 하면 못합니다. 프로그래머의 기본 소양은 모르는 것을 인터넷에서 찾는 것입니다.
또한 본인이 이해하지 못하는 코드도 사용합니다. index.js에 보시면 
mongoose.set('useNewUrlParser', true); mongoose.set('useFindAndModify', false); mongoose.set('useCreateIndex', true); mongoose.set('useUnifiedTopology', true);
라는 코드가 있죠. 저는 이부분이 뭘 하는지 정확히 모릅니다. 이 부분을 지우고 프로젝트를 실행하면 DB warning이 띄는데, 인터넷에 이 warning이 안뜨게 하는 법을 검색했고 이 네줄의 코드를 넣으라길래 넣었을 뿐입니다. 물론 더 찾아서 공부하면 이 코드들이 정확히 뭘하는지를 알 수 있겠지만 그러지 않았습니다. 제 목표는 warning이 뜨지 않게 하는 것이였고, 위 코드를 삽입하여 warning이 뜨지 않게 되었으니 그걸로 된 것입니다.
이해하지 못한채 카피만 해도 괜찮냐고 질문주셨는데, 이해하는데까지만 최대한 이해하시고 정 이해가 안되시면 넘어가도 괜찮습니다. 다음에 다른 프로젝트에서 해당 코드를 다시 사용하는 경우 다시 한번 이해하려고 시도해 보세요. 나중에 이해가 되는 날이 옵니다. 
프로젝트를 잠시 중단하고 udemy같이 다른 것을 공부하고 돌아와도 괜찮습니다. 하지만 공부하고 돌아와도 이해가 안될 수도 있습니다. 이처럼 정 이해가 안되는 부분은 일단 넘어가시고 다음에 다시 생각해도 괜찮다는 이야기를 하는 것입니다. 
하지만 개인 프로젝트를 만들며 연습하는 것은 중요합니다. 프로젝트에 본인이 이해하지 못하는 코드가 있더라도 말이죠.
J
Jake Lyu 2020.05.04
@Ian H,
친절한 답변 감사합니다. 징징거리면서 계속해야죠... ㅋㅋ 프로그래머의 기본 소양이 구글링 이라는 말씀에 용기 얻고 더 열심히 해야겠네요 ㅋㅋ 사실.. 공부하면서 기본적인 것도 다 잊어버리고 다시 찾아보는 제 자신이 좀 한심하게 느껴져서 이게 맞나 의심했거든요 ㅋㅋ 감사합니다.
I
Ian H 2020.05.04
@Jake Lyu,
이 강의의 댓글 기능은 대댓글이 무한대로 달릴 수 있게(댓글-대댓글-대댓글-대댓글..) 하기 위해 recursive를 사용했는데요, 아마 이부분이 쉽지 않으셨을 거예요.
강의의 코드를 보시면 comment-show.ejs 코드안에서 자기 자신인 comment-show를 호출하는 것을 볼 수 있는데, 이처럼 코드내에서 자기 자신의 코드를 호출하는 것을 recursive라고 합니다. (주로 함수인 경우가 많으므로 "recursive function"로 검색해보시면 자료가 많이 나옵니다.)
이 recursive는 컨셉을 이해하기가 어렵진 않지만 실제로 만들려면 굉장히 어렵습니다. (보통은 컨셉을 이해하기가 어렵고 컨셉이 이해가 되면 사용하기는 쉽죠.  예를들면 try-catch, promise, C언어의 포인터 등등)
recursive는 최대한 실전에서 많이 접하고 익히는 수 밖에 없습니다.
대댓글 앞에는 'ㄴ'을 넣고 싶다고 하셨는데, 댓글은 recursive로 구성되어서 사실 댓글과 대댓글의 구분이 없습니다. 댓글이나 대댓글이나 정확하게 같은 view 코드를 사용하고 있기 때문이죠. 하지만 댓글은 post의 show.ejs에서 호출이 되고, 대댓글은 comment-show.ejs에서 호출이 되므로 이것을 이용할 수 있습니다. 
'ㄴ'을 정확히 뭐라고 불러야할지 몰라서.. 일단 reply indicator라고 하겠습니다.
views/posts/show.ejs에는              <%- include('./partials/comment-show', {               post: post,               comment: comment,               commentForm: commentForm,               commentError: commentError,               showReplyIndicator: false,             }); %> 로, comment-show.ejs에는          <%- include('./comment-show', {           post: post,           comment: childComment,           commentForm: commentForm,           commentError: commentError,           showReplyIndicator: true,         }); %> 로 showReplyIndicator 를 false/true로 추가하시고 comment-show.ejs의 원하는 위치에     <% if(showReplyIndicator){ %><span>ㄴ</span><% } %> 를 넣고 css로 스타일을 조절해 주면 되겠습니다. 한번 시도해 보시고 안되면 또 댓글남겨주세요!
J
Jake Lyu 2020.05.05
@Ian H,
하.. ㅋㅋ 감사합니다 매번 친절한 답변 이렇게 공짜로 넙쑥넙쑥 받아도 되나 싶네요 ㅋㅋ  자애롭게 지식을 공유해주시는 프로그래머분들 너무 부럽고 멋지십니다. 리스펙!!
I
Ian H 2020.05.05
@Jake Lyu,
아닙니다. 제 얕은 지식이 다른사람에게 도움이 된다면 기쁜일이죠^^
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP