zooooss

[Reactive Native] 캐시(Cache) 설계 - 크롤링 주기 설정(속도 최적화) 본문

STUDY/App

[Reactive Native] 캐시(Cache) 설계 - 크롤링 주기 설정(속도 최적화)

zooooss 2025. 12. 3. 16:41

사용이유

1. 개발 중인 어플에서 사용자가 요소를 선택할 때마다 매번 크롤링 => 로딩시간 지연

2. 사용자가 증가한다면 크롤링 횟수가 많아져 해당 웹사이트에서 내 서버를 차단할 확률이 높아짐.

사진과 같이 상세페이지에 접속할 경우 크롤링해야하는 요소가 많아 로딩시간이 지연되었습니다.

이를 매 접속마다 크롤링 하는 것은 비효율적이며, 유저들이 머무르지 않을 것이라 생각하였습니다.

 

따라서, 일주일을 주기로 두고 한 번 크롤링을 진행한 정보들을 어딘가에 담아두고 계속 재사용(빠르게) & 7일째에 재크롤링하여 정보 업데이트(이건 7일 이후 첫 사용자에 의한게 아니라 정해진 시간에 자동 업데이트 되도록)

를 구현하기 위해 캐시를 설계하게 되었다!


Node File System 모듈 사용

1. import문 생성

서버코드에 필요한 모듈들을 import 해주었고, 이에 대한 설명은 아래와 같음!

 

  • fs/promises: Node.js의 파일 시스템을 Promise 기반으로 사용 (async/await 가능)
  • path: 운영체제에 관계없이 경로를 안전하게 다룸 (/ vs \) -> 경로 처리 유틸리티!
  • Buffer: URL을 안전한 파일명으로 변환할 때 사용

2. 캐시 디렉토리 경로 설정 및 디렉토리 생성 (+주기설정)

 

왼쪽의 폴더 및 파일들과 함께 코드를 살펴보면!
캐시 디렉토리 경로를 정해주고, 유효기간은 7일로 설정하여 캐시 디렉토리 생성을 해준다.

 

  • process.cwd(): 현재 실행 중인 디렉토리 경로
  • path.join(): 경로를 OS에 맞게 결합 (Windows: \, Mac/Linux: /)

fs.access, fs.mkdir 등 파일시스템을 사용한 문법들은 이미 import문에 정의되어있는 것들을 사용한 것이니 이는 filesystem 사용법 참조하면 됨 ! (-내가 사용한 건 아래 정리!)

fs.access(경로)

  • 역할: 파일/폴더 존재 여부 확인
  • 동작: 존재하면 성공, 없으면 에러 발생
  • 예외 처리: try-catch로 에러를 잡아서 폴더 생성 로직으로 분기

fs.mkdir(경로, { recursive: true })

  • recursive: true: 상위 폴더까지 자동 생성 (Linux의 mkdir -p와 동일)
  • 예시: crawlCache/details 생성 시 crawlCache도 자동 생성

3. 캐시 읽기 및 저장(나라별 페이지, 책별 상세페이지) + 파일명 변환

async function readCache(country) {
  try {
    const cacheFile = CACHE_FILES[country];  // 'kr' → krbooks.json
    const data = await fs.readFile(cacheFile, 'utf-8');  // 파일 읽기
    const cache = JSON.parse(data);  // JSON 파싱

    // 유효기간 검사
    const now = Date.now();
    if (now - cache.timestamp < CACHE_DURATION) {
      console.log(`✅ ${country.toUpperCase()} 캐시 사용`);
      return cache.data;  // 캐시 데이터 반환
    } else {
      console.log(`⏰ ${country.toUpperCase()} 캐시 만료됨`);
      return null;  // 만료됨 → 크롤링 필요
    }
  } catch (err) {
    console.log(`📝 ${country.toUpperCase()} 캐시 없음`);
    return null;  // 캐시 파일 없음 → 크롤링 필요
  }
}

 

  • 파일 읽기 시도 → 실패하면 catch로 이동 (파일 없음)
  • JSON 파싱 → { timestamp: 1234567890, data: [...] } 형태
  • 시간 비교 → 현재시간 - 저장시간 < 7일 확인
  • 결과 반환
    • 유효함 → 캐시 데이터 반환
    • 만료/없음 → null 반환
async function writeCache(country, data) {
  try {
    await ensureCacheDir();  // 폴더 존재 확인/생성
    const cacheFile = CACHE_FILES[country];
    
    const cache = {
      timestamp: Date.now(),  // 현재 시간 저장
      data: data,             // 크롤링 결과
    };
    
    await fs.writeFile(
      cacheFile, 
      JSON.stringify(cache, null, 2),  // 보기 좋게 포맷팅
      'utf-8'
    );
    
    console.log(`💾 ${country.toUpperCase()} 캐시 저장 완료`);
  } catch (err) {
    console.error(`❌ 캐시 저장 실패:`, err);
  }
}

 

+ 그리고 URL에는 파일명으로 사용 불가능한 문자들이 많으니 이 부분은 따로 함수로 정의

function urlToFileName(url) {
  return Buffer.from(url)
    .toString('base64')
    .replace(/[/+=]/g, '_');
}

4. 실제 API 엔드포인트 적용하는 부분

이제 정의해둔 캐시 읽기/쓰기 함수를 사용하는 kr, krdetail, us, usdetail 등의 코드를 살펴보겠습니다!

app.get('/kr-books', async (req, res) => {
  try {
    // 1. 캐시 확인
    const cachedData = await readCache('kr');
    if (cachedData) {
      return res.json(cachedData);  // 즉시 반환 (0.1초)
    }

    // 2. 캐시 없음 → 크롤링 실행
    console.log('🔄 한국 베스트셀러 크롤링 시작...');

 

크롤링 시작 아래는 기존 크롤링 로직 코드와 같다.


이렇게 성공적으로 캐시설계구현이 되었습니다!
하지만, 현재 로직에 여전히 문제점이 있는데

사용자 A 접근 -> 크롤링 -> 캐시에 담음

사용자 B 접근(7일 이내 아무때나) -> 바로 보여줌

이렇기 때문에 아직 캐시에 담기지않은 데이터에 접근한 첫 사용자는 여전히 로딩시간에 맞딱들이게 됨!

===> 사용자 접속 전 모든 것 다 미리 크롤링 후 캐시에 담는 방식으로 고쳐야 함! -> 이후 진행 예정


이 시스템으로 실시간성이 중요하지 않은 데이터를 효율적으로 관리하며,

서버 부하와 크롤링 대상 사이트의 부담을 동시에 줄일 수 있었습니다!

더하여, 다른 팀원분이 사용하신 방법으로는 "Backend/scrappers에 배치크롤러들 생성"이 있다고 하여 이후에 코드 분석 후 차이점을 비교해 볼 예정입니다.

 

++추가 개선점 : 만료 전 백그라운드로 미리 갱신해놓는 방식