차트 로딩 속도 개선 문의

고객님의 문의에 답변하는 직원은 고객 여러분의 가족 중 한 사람일 수 있습니다.
고객의 언어폭력(비하, 조롱, 욕설, 협박, 성희롱 등)으로부터 직원을 보호하기 위해
관련 법에 따라 수사기관에 필요한 조치를 요구할 수 있으며, 형법에 의해 처벌 대상이 될 수 있습니다.

커뮤니티 이용 정책에 위배되는 게시물을 작성할 경우, 별도 안내 없이 게시물 삭제 또는 커뮤니티 이용이 제한될 수 있습니다.

문의 응대 : 평일 오전 10시 ~ 오후 6시
문의를 남기실 경우 다음 항목을 작성해 주세요.
정보가 부족하거나 응대시간 외 문의하는 경우 확인 및 답변이 지연될 수 있습니다.

  • 뒤끝 SDK 버전 :
  • 프로젝트명 : 쿠노이치 키우기
  • 스테이터스 코드 :
  • 에러 코드 :
  • 에러 메시지 :

안녕하세요.
기존에 차트를 한번에 요청해서 받아왔는데, 콜백 수가 초과되는 현상이 있어서 하나씩 가져오도록 코드를 변경했습니다.
변경 후에 안정화는 되었지만 로딩 속도가 많이 느린 것 같아 문의를 남깁니다.

혹시 개선할 수 있는 방안이 있을까요?


using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using LitJson;
using BackEnd;
using TheBackend;
using System.IO;
using System.Linq;
using GameUtils;
using System.Text;
using UnityEngine.SceneManagement;

[Serializable]
public class ChartInfoData
{
public string keyName = string.Empty;
public string chartName = string.Empty;
public string chartExplain = string.Empty;
public string old = string.Empty;

public bool isChartUpload = false;
public bool isLiveChart = false;
public string chartVersion = string.Empty;

#region new
public int selectedChartFileId = 0;
#endregion

#region old
public string uuid = string.Empty;
public string inDate = string.Empty;
#endregion

public ChartInfoData(JsonData jsonData)
{
    this.chartName = jsonData.ContainsKey("chartName") ? jsonData["chartName"].ToString() : string.Empty;
    this.chartExplain = jsonData.ContainsKey("chartExplain") ? jsonData["chartExplain"].ToString() : string.Empty;
    this.old = jsonData.ContainsKey("old") ? jsonData["old"].ToString() : string.Empty;

    string[] chartNameCuts = chartName.Split('_');
    if (chartNameCuts.Length == 2)
    {
        this.keyName = chartNameCuts[0];
        if (chartNameCuts[1].Equals("Live"))
            this.isLiveChart = true;
        else if (chartNameCuts[1].Equals("Dev"))
            this.isLiveChart = false;
    }
    else if (chartNameCuts.Length == 3)
    {
        this.keyName = chartNameCuts[0];
        if (chartNameCuts[1].Equals("Live"))
            this.isLiveChart = true;
        else if (chartNameCuts[1].Equals("Dev"))
            this.isLiveChart = false;
        chartVersion = chartNameCuts[2];
    }
    else
    {
        this.keyName = chartName;
    }

    int outNum = 0;
    if (int.TryParse(jsonData["selectedChartFileId"].ToString(), out outNum))
    {
        this.isChartUpload = true;
        this.selectedChartFileId = outNum;
    }
    else
    {
        this.isChartUpload = false;
        this.selectedChartFileId = 0;
        this.uuid = jsonData.ContainsKey("uuid") ? jsonData["uuid"].ToString() : string.Empty;
        this.inDate = jsonData.ContainsKey("inDate") ? jsonData["inDate"].ToString() : string.Empty;
    }
}

}

public class CChartManager : Singleton
{
// 차트정보 데이터
List chartInfos = new();
Dictionary<int, ChartInfoData> dicChartInfoDatas = new();

// 차트 데이터
Dictionary<string, JsonData> dicChartJsonDatas = new();

public Action LoadComplete = null;
public Action LoadAllComplete = null;

int curLoadChartCount = 0;
public int CurLoadChartCount { get { return curLoadChartCount; } }

// === ADD: 이번 세션의 목표 개수를 고정(중간 변동 방지) ===
private int expectedLoadCount = 0;
public int MaxLoadChartCount { get { return expectedLoadCount; } }

public CStageData stageData;
public CMonsterData monsterData;
public CGoodsData goodsData;
public CConstData constData;
public CCharLevelData charLevelData;
public CStringData stringData;
public CCharStatData charStatData;
public CStatData statData;
public CCharStatBonusData charStatBonusData;
public CSummonData summonData;
public CEquipmentData equipmentData;
public CHiddenEquipSubStatData hiddenEquipSubStatData;
public CHiddenEquipData hiddenEquipData;
public CHiddenEquipmentLevelData hiddenEquipmentLevelData;
public CAdBuffData adbuffData;
public CBreathingStatData breathingStatData;
public CBreathingStatBonusData breathingStatBonusData;
public CIntentionStatData intentionStatData;
public CIntentionAwakenStatData intentionAwakenStatData;
public CIntentionAwakenLockData intentionAwakenLockData;
public CSkillData skillData;
public CPromoStatData promoStatData;
public CPromoMissionData promoMissionData;
public CArtifactData artifactData;
public CStageCollectionData stageCollectionData;
public CLevelCollectionData levelCollectionData;
public CDPromoData promoData;
public CDGiantData giantData;
public CPromoTitleData promoTitleData;
public CDEquipData equipDungeonData;
public CCostumeData costumeData;
public CMissionData missionData;
public CShopData shopData;
public CDBreathData breathData;
public CDSkillData skillDungeonData;
public CDGoldExpData goldExpData;
public CDIntentionData intentionDungeonData;
public CEventAttendanceData eventAttendanceData;
public CEventMissionData eventMissionData;
public CEventPassMissionData eventPassMissionData;
public CPassData passData;
public CGuideQuestData guideQuestData;
public CDForsakenData forsakenData;
public CSurpriseShopData surpriseShopData;

public bool isChartLoadComplete = false;

// === ADD: 경합/중복 방지용 세션 및 완료 집합 ===
private int loadSessionId = 0;                // 로드 세션 토큰(증가시 지난 콜백 무효화)
private HashSet<int> completedFileIds = new(); // 완료된 selectedChartFileId 모음
private bool isLoadingCharts = false;          // 한 세션의 마감 제어

public void LoadAllChartData()
{
    chartInfos.Clear();
    dicChartInfoDatas.Clear();
    dicChartJsonDatas.Clear();

    // === ADD: 세션/상태 초기화 ===
    loadSessionId++;               // 새 세션 시작
    expectedLoadCount = 0;         // 목표 개수는 GetChartList 이후 고정
    curLoadChartCount = 0;
    completedFileIds.Clear();
    isLoadingCharts = true;
    isChartLoadComplete = false;

    GetChartList();
}

void GetChartList()
{
    chartInfos.Clear();
    dicChartInfoDatas.Clear();
    bool isLiveBuild = !CGameManager.Instance.isDevelopBuild;
    int foldid = 2194;
    if (isLiveBuild)
    {
        foldid = 2442;
    }

    // 화이트리스트 체크 (동작 동일)
    SendQueue.Enqueue(Backend.Chart.GetChartContents, "201241", (callback) =>
    {
        if (callback.IsSuccess())
        {
            JsonData jsonData = callback.FlattenRows();
            for (int i = 0; i < jsonData.Count; i++)
            {
                JsonData jData = jsonData[i];
                string Nickname = GameUtils.GameUtils.TryParse<string>("Nickname", jData);
                if (CGameManager.Instance.nickName.Equals(Nickname))
                {
                    CGameManager.Instance.isWhiteList = true;
                    break;
                }
            }
        }
        else
            CGameManager.Instance.isWhiteList = false;
    });

    Backend.Chart.GetChartListByFolder(foldid, (callback) =>
    {
        if (callback.IsSuccess())
        {
            JsonData json = callback.FlattenRows();
            for (int i = 0; i < json.Count; i++)
            {
                JsonData chartData = json[i];
                ChartInfoData chartInfoData = new ChartInfoData(chartData);
                if (chartInfoData == null || chartInfoData.selectedChartFileId == 0)
                    continue;

                if (isLiveBuild == chartInfoData.isLiveChart)
                {
                    int selectedChartFileId = chartInfoData.selectedChartFileId;
                    if (dicChartInfoDatas.ContainsKey(selectedChartFileId) == false)
                        dicChartInfoDatas.Add(selectedChartFileId, chartInfoData);
                }
                chartInfos.Add(chartInfoData);
            }

            // === ADD: 이번 세션의 목표 개수 고정 ===
            expectedLoadCount = dicChartInfoDatas.Count;

            SetChartDataToLocal();
        }
        else
        {
            ShowErr(callback);
        }
    });
}

void CheckWhiteList(string selectedChartFileId, Action complete)
{
    SendQueue.Enqueue(Backend.Chart.GetChartContents, selectedChartFileId, (callback) =>
    {
        if (callback.IsSuccess())
        {
            JsonData jsonData = callback.FlattenRows();
            for (int i = 0; i < jsonData.Count; i++)
            {
                JsonData jData = jsonData[i];
                string Nickname = GameUtils.GameUtils.TryParse<string>("Nickname", jData);
                if (CGameManager.Instance.nickName.Equals(Nickname))
                {
                    CGameManager.Instance.isWhiteList = true;
                    break;
                }
            }
        }
        else
            CGameManager.Instance.isWhiteList = false;

        if (complete != null)
            complete();
        complete = null;
    });
}

void SetChartDataToLocal()
{
    StopAllCoroutines();
    StartCoroutine(CoSetChartDataToLocal());
}

void HandleLoadComplete()
{
    LoadComplete?.Invoke();
}

void LoadFinish()
{
    StartCoroutine(DelayLoadScene());
}

IEnumerator DelayLoadScene()
{
    float timer = 0f;
    while (timer < 1f)
    {
        timer += Time.unscaledDeltaTime;
        yield return null;
    }
    PlayerPrefs.Save();

    LoadAllComplete?.Invoke();

    LoadComplete = null;
    LoadAllComplete = null;
}

void ShowErr(BackendReturnObject _callback)
{
    if (_callback == null)
        ShowErr();
    else
    {
        string context = string.Format("statusCode : {0}\nerrCode : {1}\nmsg : {2}", _callback.GetStatusCode(), _callback.GetErrorCode(), _callback.GetMessage());
        ShowErr(context);
    }
}

void ShowErr(string context = null)
{
    PlayerPrefs.DeleteAll();
    if (string.IsNullOrEmpty(context))
        context = CStringManager.Instance.GetStringText("Msg_NetworkDisconnect");

    CBackendManager.Instance.BackendError.ShowErrPopup(context);
}

public void removeChartData(string _chartKey)
{
    if (dicChartJsonDatas.ContainsKey(_chartKey))
    {
        dicChartJsonDatas.Remove(_chartKey);
    }
}

public JsonData GetChartJsonData(string _chartKey)
{
    if (dicChartJsonDatas.ContainsKey(_chartKey))
        return dicChartJsonDatas[_chartKey];

    if (!isChartLoadComplete)
    {
        return null;
    }
    string chartContent = Backend.Chart.GetLocalChartData(_chartKey);

    if (string.IsNullOrEmpty(chartContent))
    {
        ShowErr(string.Format("error : {0}", _chartKey));
        return null;
    }

    JsonData jsonData = JsonMapper.ToObject(chartContent);
    if (jsonData.ContainsKey("rows"))
        jsonData = jsonData["rows"];

    if (jsonData != null)
        dicChartJsonDatas.Add(_chartKey, jsonData);

    return jsonData;
}

IEnumerator CoSetChartDataToLocal()
{
    // === ADD: 세션 토큰 캡처 (이 코루틴은 이 세션에만 유효) ===
    int session = loadSessionId;

    foreach (var item in dicChartInfoDatas.ToList())
    {
        var chartInfoData = item.Value;
        string chartKey = chartInfoData.keyName;
        int selectedChartFileId = chartInfoData.selectedChartFileId;

        // === ADD: 동일 파일 중복 증가 방지 ===
        if (completedFileIds.Contains(selectedChartFileId))
        {
            yield return null;
            continue;
        }

        int lastFileID = PlayerPrefs.GetInt(chartKey, 0);
        int curFileID = selectedChartFileId;

        // === ADD: 공통 완료 처리(세션/중복 체크 포함) ===
        Action<bool> onDone = (bool success) =>
        {
            // 지난 세션에서 온 콜백이면 무시
            if (session != loadSessionId) return;
            if (!success) { ShowErr(); return; }

            // 중복 완료 방지: 새로 추가된 경우에만 카운트 증가
            if (!completedFileIds.Add(selectedChartFileId)) return;

            curLoadChartCount += 1;

            // 방어적 클램프 (이상 증가 방지)
            if (curLoadChartCount > expectedLoadCount)
                curLoadChartCount = expectedLoadCount;

            HandleLoadComplete();

            // 모두 끝났다면 한 번만 마무리
            if (curLoadChartCount >= expectedLoadCount && isLoadingCharts)
            {
                isLoadingCharts = false;
                isChartLoadComplete = true;
                LoadFinish();
            }
        };

        if (lastFileID != curFileID)
        {
            SendQueue.Enqueue(Backend.Chart.GetOneChartAndSaveV2, selectedChartFileId.ToString(), chartKey, callback =>
            {
                if (callback.IsSuccess())
                {
                    PlayerPrefs.SetInt(chartKey, selectedChartFileId);
                    onDone(true);
                }
                else onDone(false);
            });
        }
        else
        {
            string chartData = Backend.Chart.GetLocalChartData(chartKey);
            if (string.IsNullOrEmpty(chartData))
            {
                SendQueue.Enqueue(Backend.Chart.GetOneChartAndSaveV2, selectedChartFileId.ToString(), chartKey, callback =>
                {
                    if (callback.IsSuccess())
                    {
                        PlayerPrefs.SetInt(chartKey, selectedChartFileId);
                        onDone(true);
                    }
                    else onDone(false);
                });
            }
            else
            {
                // 로컬에 이미 존재 → 즉시 완료 처리
                onDone(true);
            }
        }

        yield return null;
    }

    // === ADD: 세션/목표 기반 대기(중간에 세션 바뀌면 즉시 빠져나감) ===
    while (session == loadSessionId && curLoadChartCount < expectedLoadCount)
        yield return null;

    if (session == loadSessionId && isLoadingCharts)
    {
        isLoadingCharts = false;
        isChartLoadComplete = true;
        LoadFinish();
    }

    yield return null;
}

}

안녕하세요 개발자님,

남겨주신 차트 로딩 속도 문제에 대해 안내드립니다.

문의 주신 건은 별도 채널을 통해 이미 해결되신 것으로 확인되었으며, 동일한 현상을 겪으실 수 있는 다른 개발사분들을 위해 안내드립니다!

뒤끝 차트 조회 메소드는 버전에 따라 동작 방식에 차이가 있습니다.

  • 과거 버전 (Backend.Chart.GetChartContents, Backend.Chart.GetChartListV2)
    → 차트 개수만큼 서버에 각각 요청이 발생합니다. 차트 개수가 많아질수록 네트워크 지연 및 로딩 시간이 크게 증가합니다.
  • 최신 버전 (Backend.CDN.Content.Table.Get)
    → 여러 개의 차트를 한 번의 요청으로 일괄 조회할 수 있어, 네트워크 요청 횟수가 줄고 로딩 속도도 크게 개선됩니다.

따라서 차트 데이터를 안정적으로 빠르게 불러오기 위해서는 Backend.CDN.Content.Table.Get 메소드 사용을 권장드립니다.
신규 프로젝트나 신작 개발 시에는 반드시 해당 최신 메소드로 적용해 주시면, 차트 수가 많더라도 로딩 속도의 차이가 최소화됩니다.

감사합니다.