ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [뉴스검색봇] 5. Pagination, 코드 리펙토링
    DEVELOP/discord-bot 2022. 5. 12. 01:50

    이전에 작성한 글에 이어,

    이번엔 페이지네이션을 어떻게 구현했는지에 대해 작성할 것이다.

     

    1. 첫번째 방법

    페이지가 넘어갈 때마다 api쿼리의 start값을 10씩 증가시키면서 호출하자.

     

    그러나 이 방법을 생각한지 얼마 지나지 않아, 대단히 비효율적이라는 것을 깨달았다.

    왜냐하면 start값을 변경할 때마다, 즉 사용자가 버튼을 클릭할 때마다 api를 계속 호출해야하기 때문이다.

    이는 내가 생각하기에도...좀...아닌거 같았다.

    이렇게 만들면 사용자가 얼마 안돼도 api호출량이 기하급수적으로 늘어날 것이다.

    이 방법의 장점이 있긴 있다.

    버튼을 계속 클릭할 수만 있다면 만개든 10만개든 모두 읽어올 수 있다는 거..?

     

    2. 두번째 방법

    api를 키워드별로 딱 한번만 호출하되, display값을 100으로 설정해서 100개의 뉴스기사만 읽어온다.

    그 후 인덱스를 이용해서 10개씩 끊어서 출력한다.

     

    이렇게 하면 1번 방법에 비해 api호출량을 대폭 줄일 수 있다.

    어처피 최신 뉴스 위주로 검색할 것이고, 캐주얼한 대화가 오가는 디스코드의 특성상 100개도 충분히 많은 양이라고 생각했다. 솔직히 50개로 해도 충분하다 생각한다.

     

    이전에 뉴스데이터들이 items에 배열로 저장되는 것을 확인했기 때문에 두번째 방법을 바로 생각할 수 있었다.

     


    페이지 설계

     

    pageNum, selectedValue 두개의 변수를 선언했다.

    pageNum은 페이지 번호이고, selected value는 사용자가 선택한 메뉴에 따라 변경되는 값이다.

     

    페이지 단위를 10개씩 나누기로 했으니, 버튼을 클릭할 때마다 pageNum을 10씩 증감한다.

    이 때 값이 0보다 작아지거나 90보다 커지면, 가져온 100개의 데이터를 초과하기 때문에 예외처리를 해줘야한다.

     

    await collector.on("collect", async (interaction) => {
                    
                    if ((await interaction.customId) === "nextPage") {
                        if(pageNum >= 90) {
                            await interaction.reply( { content : "마지막 페이지 입니다.", ephemeral: true });
                            // await wait(2000);
                            // await interaction.deleteReply();
    
                            pageNum = 90;
    
                            return;
                        }
    
                        pageNum += 10;
                        selectedValue = pageNum;
    
                        console.log('pageNum = ', pageNum);
                        console.log('selectedValue = ', selectedValue);
                    }
    
                    if ((await interaction.customId) === "previousPage") {
                        if(pageNum <= 0) {
                            await interaction.reply( { content : "첫번째 페이지 입니다.", ephemeral: true });
                            // await wait(2000);
                            // await interaction.deleteReply();
    
                            pageNum = 0;
                            return;
                        }
    
                        pageNum -= 10;
                        selectedValue = pageNum;
    
                        console.log('pageNum = ', pageNum);
                        console.log('selectedValue = ', selectedValue);
                    }
                    
    ...

     

    다음페이지와 이전페이지 버튼을 눌렀을 때의 코드다.

    10씩 증감하다가 pageNum이 90이상이거나 0이하이면 페이지값을 더 증감하지 않고 이전값으로 대입한 후,

    아래 문장이 실행되지 않도록 리턴해줬다.

     

    reply에서 ephemeral은 "나만보이는 메시지" 설정 옵션이다.

    그냥 메시지를 출력 후 시간이 지나면 삭제되도록 하는 것과 메시지를 ephemeral로 만드는 것 둘 중에서 고민중이다.

    전자로 하려면 ephemeral 프로퍼티를 지우고 주석을 해제하면 된다.

     

    * wait을 쓰기 위해선 임포트가 필요하다. 상단에 해당 코드 입력

    const wait = require('node:timers/promises').setTimeout;

     

     

    selectedValue에 그대로 페이지값을 넘겨주는 이유는 같은 페이지 내에서 페이지값은 그대로 보존하고, selected값만 변경하도록 하기 위해서다.

     

     

     

    다음으로 사용자가 셀렉트 메뉴를 클릭했을 때 selectValue의 값을 변경해주는 코드를 작성했다.

    // in Collector.on
    if ((await interaction.customId) === "select") {
                        selectedValue = parseInt(interaction.values[0]) + pageNum;
                        console.log(selectedValue);
        }

    셀렉트 메뉴를 만들 때 return값을 숫자로 한 이유가 이렇게 인덱스 접근을 쉽게 하기 위해서이다.

    내가 만든 메뉴는 다중선택이 아닌 단일선택이기 때문에, 메뉴를 선택하면 값이 하나만 전달된다.(0번째 인덱스)

    형식이 문자열이기 때문에 parseInt로 정수변환 해준 후, pageNum을 더해준다.

    이 값을 뉴스데이터의 items배열의 인덱스로 사용하면 원하는 데이터를 얻어올 수 있다.

     


    임베드에 선택한 데이터대로 뉴스를 출력해주기 위해 getEmbed함수를 수정했다.

     

    getEmbed: async function (data, selectedValue, pageNum) {
            const { MessageEmbed } = require("discord.js");
            const rt = require("./removeTags.js");
    
            pageNum = parseInt(pageNum / 10) + 1;
            currentPage = (selectedValue % 10) + 1;
            data.items[selectedValue].pubDate = data.items[selectedValue].pubDate.replace("+0900", "");
    
            return new MessageEmbed()
                .setColor("#2DB400")
                .setTitle(rt.removeTags(data.items[selectedValue].title))
                .setURL(data.items[selectedValue].link)
                .setThumbnail("https://cdn.discordapp.com/attachments/973929274744643595/973929352402190356/-001_5.png")
                .addFields({
                    name: "ㅤ",
                    value: rt.removeTags(data.items[selectedValue].description),
                })
                .setFooter({
                    text: "Published :: " + data.items[selectedValue].pubDate + " (" + currentPage + " of " + pageNum + " page)",
                });
        },

    매개변수로 새로 만든 pageNum과 selectedValue를 추가했다.

    items의 인덱스 부분을 selectedValue로 모두 교체했다.

     

    pageNum과 currentPage는 단순히 사용자에게 보여지는 부분을 위한 값이다.

    1씩 더해주는 이유는 인덱스는 0부터 시작하지만 화면에는 1부터 출력하는 것이 보기 좋기 때문이다.

    내가 클릭한 기사가 몇 번째 페이지의 몇 번째 기사인지 알 수 있도록 footer에 정보를 추가했다.

     


    getSelectMenus함수도 페이지에 맞춰서 10개씩 기사를 보여주어야 하므로 수정했다.

     

    getSelectMenus : async function(data, pageNum) {
            const { MessageActionRow, MessageSelectMenu } = require("discord.js");
            const rt = require("./removeTags.js");
            const page = parseInt(pageNum/10) + 1
    
            return new MessageActionRow().addComponents(
                new MessageSelectMenu()
                    .setCustomId("select")
                    .setPlaceholder("뉴스기사를 선택해주세요.")
                    .addOptions([
                        {
                            label: rt.removeTags(data.items[pageNum + 0].title),
                            description: "1 of " + page + " Page",
                            value: "0",
                        },
                        {
                            label: rt.removeTags(data.items[pageNum + 1].title),
                            description: "2 of " + page + " Page",
                            value: "1",
                        },
                        
                        ...

     

    매개변수로 pageNum을 추가하고, 각각 0~9를 더해준 값을 items의 인덱스로 넣어준다.

    같은 코드의 반복이기 때문에 나머지는 생략했다.

     

    page변수는 embed와 마찬가지로 출력을 위한 변수다.

    description(설명)에 페이지정보를 입력했다.

     


    상호작용이 일어나면, 임베드와 메뉴를 새로운 값으로 수정해서 얻어오고, 수정한 내용으로 메시지를 업데이트하는 코드를 collector.on함수의 마지막 부분에 작성했다. 이렇게 하면 동작 하나의 수행이 끝날 때마다 이 마지막 문장이 수행되면서 임베드를 데이터에 맞게 변경한다.

     

       ...
        embed = await ge.getEmbed(data, selectedValue, pageNum);
        row = await gsm.getSelectMenus(data, pageNum);
    
            try {
                // await interaction.deferUpdate();
                await interaction.update({
                    content: ":mag: `" + keyword + "` 로 검색한 결과입니다.",
                    embeds: [embed],
                    components: [row, row2],
                });
    
                ...
    
    }); // end of collector

     

    interaction.update를 쓰면 기존에 보냈던 메시지를 수정한다.

    interaction.reply나 interaction.channel.send는 새로운 메시지를 보낸다.

    클릭할 때마다 메시지를 새로 보내는 것보다 진짜 검색창처럼 기존 메시지 안에서 동작하는게 보기 좋아서 update를 사용했다.

    row2는 참고로 버튼객체를 담은 변수이름이다.

     


    코드 리펙토링하기

     

    commands파일을 따로 분리하는 것은 디스코드 가이드에서 알려줘서 그렇게 했는데,

    그 파일 내에서 또 모든 기능을 다 쭉 작성하니 별 것도 안했는데 코드가 300줄이 넘어가기 시작했다😑

    오른쪽 뷰를보면 알 수 있듯 스멀스멀 올라오는 노답기운. 원래는 주석이 없다. 저 부분이 완전히 동일한 코드였다..

    이대로 계속 하다간 진짜 하루 자고 일어나는 순간 수정도 못하겠구나 싶어서 리펙토링을 했다.

     

    말이 리펙토링이지 그냥 함수로 묶어놓은게 다다.

     

    기능별로 묶어서 commands파일이랑 똑같이 functions로 만들었다.

    module.exports = {
        함수이름 : async function(매개변수가 있다면 이안에) {
         // 실행문
        },
        
        이것도함수 : async function(매개변수) {
         // 실행문
        },
    };

    이 안에 함수로 만들고 싶은 코드들을 그대로 복사해오면 된다.

    해당 실행문이 require을 필요로 할경우 꼭 같이 넣어줘야한다. 보통 코드를 작성할 때 최상단에 적어놓기 때문에 빼먹는 경우가 많다.

     

    그리고 명령어 파일 안에서 해당 기능들을 불러올 땐

    const 변수명 = require("함수파일 경로");

    execute안에 require로 불러온 후 변수명.함수이름으로 사용하면 된다.

     

    이렇게 하니 거의 350줄정도 되는 코드가 3분의 1로 줄었다.

    지금은 기능을 더 추가해놓은 상태인데도 150줄가량 된다.

     

    그래도 아직 깔끔한 코드와는 거리가 멀다.

    함수 뿐 아닌 이벤트도 따로 파일로 관리할 수 있는데, 당장 이해하기 너무 복잡했고 아직 해야할게 많이 남았기 때문에 이쯤에서 만족하기로 했다.

    댓글

Designed by Tistory.