본문 바로가기
컴퓨터

SEO 최적화를 위해 사이트 맵을 생성해보자(사이트맵 최적화)

by 도도새 도 2025. 8. 10.

사이트맵을 최적화해보자!

 

현재 내 사이트 머그인(https://www.mug-in.com/)에서는 프론트엔드가 배포 될 때마다 사이트맵을 생성하고있다. 이는 SEO 최적화를 위함인데, 구글봇이 주기적으로 내 사이트를 방문하여 사이트맵을 읽고, 각 경로에 접속해 유효한 색인과 유효하지 않은 색인을 구분해준다.

사이트맵이란? 사이트맵은 검색 엔진에 크롤링 가능한 사이트 페이지를 알리는 간편한 방법이다. 가장 간단한 형태의 사이트맵은 사이트 URL과 각 URL에 대한 추가 메타데이터(마지막 업데이트 날짜, 일반적인 변경 빈도, 사이트 내 다른 URL 대비 중요도)를 나열한 XML 파일로, 검색 엔진이 사이트를 더욱 지능적으로 크롤링할 수 있도록 한다. 웹 크롤러는 일반적으로 사이트 내의 링크와 다른 사이트에서 페이지를 검색한다. 사이트맵은 이러한 데이터를 보완하여 사이트맵을 지원하는 크롤러가 사이트맵의 모든 URL을 수집하고 관련 메타데이터를 사용하여 해당 URL에 대한 정보를 얻을 수 있도록 돕는다. 사이트맵 프로토콜을 사용한다고 해서 웹 페이지가 검색 엔진에 포함되는 것은 아니지만, 웹 크롤러가 사이트를 더 효과적으로 크롤링할 수 있도록 힌트를 제공하여 SEO를 최적화한다.

 
현재는 next-sitemap이라는 라이브러리를 사용하여 설정 파일을 중심으로 내 next.js 프로젝트가 vercel에 배포될 때마다 사이트맵을 생성하고 있다.
사용은 아래와 같이 하고 있다.
 

현재 사이트맵 생성 방식

 

next-sitemap 사용

  1. 사이트맵 설정파일 생성
  • 파일명 : next-sitemap.config.js
  • 프로젝트 루트에 생성한다.
module.exports = {
  siteUrl: process.env.NODE_ENV === 'production' ? '(...)' : '<http://localhost:3000>',
  additionalPaths: async (config) => {
    // 레시피 ID 리스트 가져오기
    const recipe_res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}recipe/recipe-id-list`);
    const recipeIdList = await recipe_res.json();

    // 보드 사이트맵 ID 리스트 가져오기
    const board_res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}board/sitemap-board-id`);
    const board_sitemap_res = await board_res.json();

    const paths = [];

    // 레시피 경로 추가
    for (let i = 0; i < recipeIdList.length; i++) {
      paths.push(await config.transform(config, `/recipe-detail/${recipeIdList[i]}`));
    }

    // 보드 경로 추가
    for (let i = 0; i < board_sitemap_res.length; i++) {
      paths.push(await config.transform(config, `/board/${board_sitemap_res[i].menuId}/detail/${board_sitemap_res[i].boardId}`));
    }

    console.log("사이트맵 확인 ", paths);
    return paths;
  },
  generateRobotsTxt: true, // robots.txt 생성
  sitemapSize: 5000, 
  changefreq: 'daily',
  priority: 0.7, 
  exclude: ['/admin/**'],
};

해당 설정이 완료되면 아래 명령어를 실행하여 사이트맵을 생성할 수 있다.

  • npx next-sitemap

현재는 pacak.json파일 내에 빌드가 완료될 때 위 명령어를 실행하여 자동으로 사이트맵이 생성되도록 하고 있다.

  1. 사이트맵 자동 빌드
  • 파일명 : package.json
    • 라이브러리 이름, 버전 정보
    • 의존 관리
    • 명령어 단축 스크립트 정의
{
  "name": "recipe-front",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "https-dev": "node server.js",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "postbuild": "next-sitemap" //빌드 후 실행
  },
  "dependencies": {
    (...)
  } 
}
  • postbuild는 빌드 이후 자동으로 실행되는 스크립트를 정의한다.
  • 즉 npm run build를 통해 next build가 실행되면 next-sitemap이 실행된다.
  • 이를 통해 위에서 한 설정을 따라 sitemp파일이 생성된다.

 

나의 요구사항과 수정 사항

 

나의 요구 사항

위 기능으로 여태까지 사이트를 유지하고 있었으나, 아래 나열한 사항들에 대해 불편함을 느껴 사이트맵 생성을 수정하고자 한다.

  1. 새로운 게시글이 게시되어도 사이트맵이 새로 생성되지 않는다.(새로 빌드 및 배포 시까지 사이트맵은 유지된다.)
  2. 사이트맵이 변경되어도 구글 및 네이버가 내 사이트가 업데이트 되었다는 사실을 모른다.

 

수정 사항

  1. 게시글이 생성되면 사이트맵에 새로 추가하여 새 사이트맵을 생성한다(10분 캐싱)
  2. 사이트맵은 스프링 서버에서 생성해서 next.js서버에서 받아서 처리한다.

 
자바 사이트맵 생성
서칭 후 dfabulich/sitemapgen4j라는 라이브러리를 사용하려고 한다.

가장 쉬운 사이트맵 생성 방법

WebSitemapGenerator wsg = new WebSitemapGenerator("<http://www.example.com>", myDir);
wsg.addUrl("<http://www.example.com/index.html>"); // repeat multiple times
wsg.write();

위 코드를 베이스로 아래와 같이 테스트 코드를 작성해보자.

@Slf4j
@RequestMapping("/seo")
@Controller
public class SEOController {

    @GetMapping("/sitemap")
    public void generateSitemap(){
        try{
            File tempDir = new File(System.getProperty("java.io.tmpdir"));
            String baseUrl = "<http://www.mug-in.com>";

            //sitemap 생성
            WebSitemapGenerator wsg = new WebSitemapGenerator(baseUrl, tempDir);
            wsg.addUrl(baseUrl + "/test1.html");
            wsg.addUrl(baseUrl + "/test2.html");

            wsg.write();
            System.out.println("사이트맵 생성 완료! 위치: " + tempDir.getAbsolutePath());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

결과

  • 사이트맵 생성 완료! 위치: C:\Users\JUI~1\AppData\Local\Temp
  • 해당 경로에 들어가보면 sitemap.xml라는 파일이 있고, 열어보면 아래와 같이 사이트맵이 생성되어있다.
  •  
사이트맵 생성

 
내가 사이트맵 최적화를 위해 해야 할 것은 아래와 같다.

  1. next.js의 /sitemap.xml 페이지에 접속시 백엔드로 라다이렉팅한다
  2. 백엔드에서는 사이트맵을 응답한다.
    1. 이때, 사이트맵은 스트링 형태로 반환한다.(캐싱을 용이하게 관리하기 위함)

 

사이트맵 생성 이관

다행이 next.js 설정 자체는 어렵지 않다.

module.exports = {
  async rewrites() {
    return [
      {
        source: '/sitemap.xml',
        destination: '내 백엔드 사이트맵 생성 api 경로',
      },
    ]
  },
}

이제 위 두 부분을 합쳐서 사이트맵을 생성한다.
 

API 서버 코드

controlller

@Slf4j
@Controller
@RequestMapping("/seo")
@RequiredArgsConstructor
public class SEOController {
    private final SEOService seoService;

    @GetMapping("/sitemap")
    public ResponseEntity<String> generateSitemap() throws IOException {
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_XML)
                .body(seoService.getSiteMap());
    }
}

 
service

@Slf4j
@Service
@RequiredArgsConstructor
public class SEOServiceImpl implements SEOService {
    private final SEORedisRepository seoRedisRepository;
    private final RecipeRepository recipeRepository;
    private final BoardRepository boardRepository;

    @Value("${front_url}")
    private String frontUrl;

    public String getSiteMap() throws IOException {
        String cachedXMLSiteMap = seoRedisRepository.getSitemap();

        // 캐싱된 사이트맵이 있는 경우 반환
        if(cachedXMLSiteMap != null){
            return cachedXMLSiteMap;
        }
        
        // 캐싱 된 사이트맵이 없는 경우 새로 생성 + 캐싱
        File tempDir = new File(System.getProperty("java.io.tmpdir"));

        //sitemap 생성
        WebSitemapGenerator wsg = new WebSitemapGenerator(frontUrl, tempDir);
        setBoardUrlToWsg(wsg);
        setRecipeUrlToWsg(wsg);

        wsg.write();// 기존에 사이트맵 파일 있을 시 덮어쓰기

        log.info("[getSiteMap] - 사이트맵 생성 완료 - 위치: " + tempDir.getAbsolutePath());
        Optional<File> sitemapFileOpt = Arrays.stream(Objects.requireNonNull(tempDir.listFiles()))
                .filter(f -> f.getName().startsWith("sitemap") && f.getName().endsWith(".xml"))
                .max(Comparator.comparingLong(File::lastModified));

        if (sitemapFileOpt.isEmpty()) {
            throw new BusinessException(ErrorCode.SITEMAP_FILE_NOT_FOUND);
        }

        File sitemapFile = sitemapFileOpt.get();
        String sitemapXml = Files.readString(sitemapFile.toPath(), StandardCharsets.UTF_8);

        // 캐싱
        seoRedisRepository.setSitemap(sitemapXml, 10);

        return sitemapXml;
    }

    public void setBoardUrlToWsg(WebSitemapGenerator wsg){
        List<Board> boardList = boardRepository.getNotDeletedBoardList();

        List<BoardSiteMapDTO_OUT> boardSiteMapDTOList = boardList.stream().map((board)-> BoardSiteMapDTO_OUT.builder()
                .boardId(board.getBoardId())
                .menuId(board.getBoardMenu().getBoardMenuId())
                .build()).toList();

        for(var boardSiteMap:boardSiteMapDTOList){
            wsg.addUrl(frontUrl + "board/" + boardSiteMap.getMenuId() + "/detail/" + boardSiteMap.getBoardId());
        }
    }

    public void setRecipeUrlToWsg(WebSitemapGenerator wsg){
        List<Long> recipeIdList =  recipeRepository.getNotDeletedRecipeList()
                .stream().map(Recipe::getId)
                .toList();

        for(Long recipeId:recipeIdList){
            wsg.addUrl(frontUrl + "recipe-detail/" + recipeId);
        }
    }
}

 
이제 해당 프로젝트를 실행 후 사이트 맵을 보면, 아래와 같이 사이트맵이 잘 생성 된 것을 알 수 있다.


<script/>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/2>https://www.mug-in.com/board/1/detail/2</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/3>https://www.mug-in.com/board/1/detail/3</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/4>https://www.mug-in.com/board/1/detail/4</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/5>https://www.mug-in.com/board/1/detail/5</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/6>https://www.mug-in.com/board/1/detail/6</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/7>https://www.mug-in.com/board/1/detail/7</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/2/detail/9>https://www.mug-in.com/board/2/detail/9</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/10>https://www.mug-in.com/board/1/detail/10</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/12>https://www.mug-in.com/board/1/detail/12</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/15>https://www.mug-in.com/board/1/detail/15</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/16>https://www.mug-in.com/board/1/detail/16</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/17>https://www.mug-in.com/board/1/detail/17</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/18>https://www.mug-in.com/board/1/detail/18</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/19>https://www.mug-in.com/board/1/detail/19</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/20>https://www.mug-in.com/board/1/detail/20</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/21>https://www.mug-in.com/board/1/detail/21</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/22>https://www.mug-in.com/board/1/detail/22</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/23>https://www.mug-in.com/board/1/detail/23</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/24>https://www.mug-in.com/board/1/detail/24</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/25>https://www.mug-in.com/board/1/detail/25</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/26>https://www.mug-in.com/board/1/detail/26</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/27>https://www.mug-in.com/board/1/detail/27</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/28>https://www.mug-in.com/board/1/detail/28</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/29>https://www.mug-in.com/board/1/detail/29</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/30>https://www.mug-in.com/board/1/detail/30</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/32>https://www.mug-in.com/board/1/detail/32</a>></loc>
</url>
<url>
<loc><<a href=https://www.mug-in.com/board/1/detail/33>https://www.mug-in.com/board/1/detail/33</a>></loc>
</url>
<url>
(...)

이제 프론트에도 위 설정을 마친 후, 배포하게 되면?

프론트로 들어갔지만 백엔드에서 보내준 sitemap이 화면에 보이게 된다.
이제 특정 게시글이 배포되면  10분간만 기존 사이트맵을 캐싱하고, 새로운 게시글 정보가 포함된 사이트맵을 봇에게 제공해 줄 것이다!

댓글