그누보드와 Flutter 앱, JWT로 회원 연동하기: 레거시 시스템 통합 경험기
안녕하세요, 코딩아빠입니다. 이번 글은 제가 최근 직접 부딪히고 해결했던 개발 경험을 정리한 기록입니다. 레거시 PHP CMS인 그누보드 웹사이트의 회원 체계를 그대로 활용하면서 Flutter 앱에서 자체 로그인, 회원가입, 토큰 갱신 기능을 구현해야 했던 상황이었죠. 기존 시스템의 작동 방식을 깊이 이해하는 것이 얼마나 중요한지 다시 한번 깨달았고, 특히 include 순서나 전역 변수 사용 방식 같은 사소한 부분이 예상치 못한 큰 오류를 발생시킬 수 있다는 교훈을 얻었습니다. 이 경험을 통해 얻은 노하우를 공유해 드립니다.
이 글에서 짚는 것
- 레거시 그누보드 환경에서 PHP 기반 API를 구축하는 방법
- JWT(JSON Web Token)를 활용한 앱 인증 시스템 구현 원리
- 그누보드
common.php및lib.php로딩의 중요성과 올바른 적용법 - 기존 웹사이트 회원 데이터베이스를 앱과 연동하는 실제 과정
오래된 그누보드, 새로운 Flutter 앱과의 만남 준비
새로운 Flutter 앱을 개발하면서 기존에 운영하던 그누보드 웹사이트의 회원 체계를 그대로 사용해야 하는 상황에 놓였습니다. 별도의 소셜 로그인 연동 없이, 웹과 앱 사용자가 동일한 아이디와 비밀번호로 로그인하고 회원 정보를 공유하는 통합 시스템이 필요했죠. 이는 기존 회원의 불편을 최소화하고 데이터 일관성을 유지하기 위한 중요한 결정이었습니다. 가장 먼저 고민했던 부분은 바로 어떻게 기존 그누보드 회원 데이터베이스를 안전하고 효율적으로 앱과 연동할 것인가였습니다.
그누보드가 PHP 기반의 레거시 CMS라는 점은 분명 도전 과제였습니다. 자체 라이브러리 로딩 방식과 전역 변수 의존성이 높다는 특성 때문에, 신규 API 개발 시에는 기존 그누보드 환경을 해치지 않으면서도 필요한 기능을 추가해야 했으니까요. 특히 기존 웹사이트의 로그인 로직을 직접 복제하거나 수정하는 대신, 앱만을 위한 별도의 인증 시스템을 구축하여 보안성과 확장성을 확보하는 방향으로 가닥을 잡았습니다. 여기에는 JWT(JSON Web Token)를 활용하는 것이 가장 적합하다고 판단했지요.
JWT는 클라이언트와 서버 간의 정보 교환 시 사용되는 안전한 방법으로, 서버가 상태를 유지할 필요가 없어 확장성이 좋다는 장점이 있습니다. 앱에서 로그인 요청을 보내면, 서버는 회원 정보를 확인하고 유효한 경우 JWT를 발급하여 앱에 넘겨주는 방식입니다. 이후 앱은 이 토큰을 매 요청마다 헤더에 포함하여 보내고, 서버는 토큰의 유효성을 검증하여 인증된 사용자임을 확인하는 것이죠. 이러한 접근 방식은 레거시 시스템의 복잡성을 최소화하면서도 현대적인 앱 인증 시스템을 구축할 수 있는 좋은 대안이었습니다.
그누보드 환경 위에서 API 엔드포인트 설계하기
JWT 기반 인증 시스템을 구현하기 위해 서버에 몇 가지 새로운 API 엔드포인트를 구축해야 했습니다. 구체적으로는 로그인(api/app/login.php), 회원가입(api/app/register.php), 그리고 토큰 갱신(api/app/refresh.php) 기능을 담당하는 PHP 파일을 새로 만들었죠. 이 파일들은 그누보드 설치 경로 내에 별도의 디렉터리를 만들어 관리함으로써 기존 그누보드 코드와의 충돌을 피하려고 했습니다.
각 엔드포인트는 그누보드의 g5_member 테이블을 직접 조회하거나 업데이트하는 방식으로 작동합니다. 예를 들어, 로그인 요청이 오면 전달받은 아이디와 비밀번호를 g5_member 테이블의 데이터와 비교하여 일치 여부를 확인하는 식입니다. 여기서 가장 중요했던 부분은 바로 '그누보드 환경을 올바르게 초기화하는 것'이었습니다. 그누보드 핵심 라이브러리들을 제대로 로드하지 않으면 데이터베이스 연결이나 기타 전역 변수들이 초기화되지 않아 예상치 못한 오류를 뱉어내더군요.
이를 해결하기 위해 각 API 파일의 최상단에 define('_GNUBOARD_', true);를 선언하고, 이어서 include_once('../../common.php');를 호출하는 순서를 엄격하게 지켰습니다. 이 common.php 파일은 그누보드의 거의 모든 환경 설정과 핵심 함수들을 로드하는 역할을 하죠. 이 한 줄이 빠지거나 순서가 틀리면, 그누보드 환경이 제대로 잡히지 않아 데이터베이스 연결조차 되지 않는 문제가 발생했습니다. 제가 처음 겪었던 시행착오 중 하나였습니다.
<?php
define('_GNUBOARD_', true);
include_once('../../common.php'); // 그누보드 환경 초기화
// JWT 라이브러리 로드 및 API 로직 구현
이렇게 common.php를 로드한 후에는 비로소 그누보드의 데이터베이스 연결 객체($g5['db'])나 다른 유틸리티 함수들을 사용할 수 있게 됩니다. 이 부분이 레거시 시스템 위에서 새로운 기능을 개발할 때 가장 먼저 해결해야 할 퍼즐 조각이었습니다.
JWT 발급과 갱신, 그리고 그누보드 회원 연동의 핵심
그누보드 환경이 제대로 초기화되었다면, 이제 실제 로그인 로직을 구현하고 JWT를 발급하는 단계로 넘어갑니다. 앱에서 아이디와 비밀번호를 POST 요청으로 보내면, 서버에서는 이를 받아 g5_member 테이블에서 해당 회원 정보를 조회합니다. 비밀번호는 그누보드의 해싱 방식에 맞춰 검증해야 하므로, 기존 그누보드 로그인 로직에서 사용되던 함수를 활용하는 것이 안전하고 정확합니다.
인증이 성공하면, 해당 회원의 고유 식별자(예: mb_id)를 포함하는 JWT 페이로드를 생성합니다. 이 페이로드에는 토큰의 만료 시간(exp)과 같은 정보도 함께 담습니다. 만료 시간은 앱의 보안 정책과 사용자 편의성을 고려하여 적절히 설정하는 것이 중요합니다. 너무 짧으면 사용자가 자주 로그인해야 해서 불편하고, 너무 길면 보안에 취약해질 수 있으니까요. 발급된 JWT는 암호화되어 서명되며, 이 토큰을 HTTP 응답으로 앱에 전달합니다.
// JWT 발급 예시 (실제 라이브러리 코드는 복잡함)
$payload = ['user_id' => $member['mb_id'], 'exp' => time() + 3600];
$jwt = 'YOUR_GENERATED_JWT_TOKEN';
앱은 이 JWT를 안전하게 저장하고, 이후 보호된 API를 호출할 때마다 HTTP Authorization 헤더에 Bearer {JWT} 형태로 포함하여 보냅니다. 또한, 장기적인 사용자 경험을 위해 리프레시 토큰(Refresh Token) 메커니즘도 함께 구현했습니다. 액세스 토큰이 만료되면, 앱은 저장된 리프레시 토큰을 이용해 새로운 액세스 토큰을 요청하는 방식으로, 사용자가 다시 로그인할 필요 없이 세션을 연장할 수 있도록 했습니다. 이 과정 역시 api/app/refresh.php 엔드포인트를 통해 처리됩니다.
보호된 API 접근과 토큰 유효성 검증 과정
JWT를 발급하고 나면, 앱은 이 토큰을 사용하여 회원 전용 기능과 같은 보호된 API에 접근하게 됩니다. 서버는 이러한 보호된 API 요청이 들어올 때마다 클라이언트로부터 전달받은 JWT의 유효성을 검증해야 합니다. 이 검증 과정은 보안에 있어 매우 중요한 단계이며, 토큰이 위조되지 않았는지, 만료되지는 않았는지 등을 확인합니다.
일반적으로 JWT는 HTTP Authorization 헤더에 Bearer 스키마와 함께 전달됩니다. 서버 측 API에서는 먼저 이 헤더에서 토큰 문자열을 추출하는 작업이 필요합니다. $_SERVER['HTTP_AUTHORIZATION'] 변수에서 값을 읽어와 Bearer 접두사를 제거하면 순수한 JWT를 얻을 수 있습니다. 만약 토큰이 없거나 형식이 올바르지 않으면 즉시 인증 실패를 처리해야 합니다.
// JWT 검증 예시 (실제 라이브러리 코드는 복잡함)
$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (strpos($token, 'Bearer ') === 0) {
$token = substr($token, 7);
}
// if (isValidJwt($token)) { ... } else { unauthorized }
추출된 토큰은 JWT 라이브러리를 사용하여 서명 검증, 만료 시간 확인, 페이로드 유효성 검사 등의 과정을 거칩니다. 토큰의 서명이 유효하지 않거나 이미 만료된 토큰이라면, 해당 요청은 인증되지 않은 것으로 간주하여 401 Unauthorized 응답을 반환합니다. 이 과정을 통해 오직 유효한 토큰을 가진 사용자만이 보호된 리소스에 접근할 수 있도록 보장할 수 있었지요. 이처럼 서버에서 철저하게 토큰을 검증하는 것이 시스템 전체의 보안을 유지하는 핵심이었습니다.
실제로 작동하는 시스템을 위한 꼼꼼한 확인
모든 API 엔드포인트 개발과 JWT 로직 구현을 마친 후, 실제로 시스템이 제대로 작동하는지 검증하는 과정이 필요했습니다. 단순히 코드가 에러 없이 실행되는 것을 넘어, Flutter 앱에서 실제 사용자 흐름에 맞춰 로그인, 회원가입, 토큰 갱신 기능을 사용했을 때 예상대로 동작하는지 확인하는 것이 중요했습니다. 저는 단계별로 꼼꼼하게 테스트 시나리오를 만들고 하나씩 점검해 나갔습니다.
먼저, 새로운 계정으로 회원가입을 시도하여 그누보드 g5_member 테이블에 새로운 레코드가 성공적으로 추가되는지 확인했습니다. 다음으로, 방금 가입한 계정으로 로그인 기능을 테스트하여 유효한 JWT가 발급되고 앱으로 전달되는지 응답 값을 면밀히 살펴보았죠. 발급된 JWT는 앱의 로컬 스토리지에 잘 저장되는지, 그리고 만료 시점에 맞춰 토큰 갱신 API가 호출되어 새로운 토큰을 받아오는지도 확인했습니다.
가장 중요한 검증 단계는 바로 발급된 JWT를 사용하여 다른 보호된 API(예: 회원 정보 조회 API)에 접근했을 때였습니다. 앱이 토큰을 HTTP Authorization 헤더에 포함하여 요청을 보냈을 때, 서버에서 이 토큰의 유효성을 성공적으로 검증하고 올바른 데이터를 반환하는지 확인했습니다. 만료된 토큰으로 접근을 시도했을 때는 401 Unauthorized 응답이 정확히 오는지도 확인하여, 인증 시스템이 설계대로 보안적으로도 작동하는지 검증할 수 있었습니다. 이 모든 과정을 거치면서 레거시 시스템과의 연동이 성공적으로 마무리되었음을 확인할 수 있었네요.
정리하며
오래된 그누보드와 현대적인 Flutter 앱을 JWT 기반으로 연동하는 작업은 분명 쉽지 않은 과정이었습니다. 특히 레거시 시스템의 특성을 이해하고 그 위에서 새로운 기능을 안정적으로 구현하는 것이 관건이었죠. 이번 경험을 통해 저는 다시 한번 '기존 시스템의 작동 원리와 의존성을 깊이 파악하는 것이 중요하다'는 교훈을 얻었습니다. 모든 개발은 새로운 것을 만드는 즐거움도 있지만, 기존의 것을 존중하고 이해하는 데서 더 큰 가치를 찾을 때도 있다는 점을 잊지 말아야겠습니다. 같은 문제를 겪는 분들께 도움이 되었기를 바랍니다.

Top comments (0)