토이 프로젝트/ChatForYou V2 프로젝트

Spring Boot Web Chatting v2: 스프링 부트로 실시간 화상 채팅 만들기(3) 소셜로그인 기능 개발(feat. QR 로그인)

TerianP 2025. 11. 21.
728x90

1. 시작하면서

정말 오랜만에 포스팅 쓰는 거 같습니다. 저는 사실 얼마 안 지났다고 생각했는데 벌써  2개월이 지나버렸네요. "시간은 쏘아진 화살과 같다"라고 하는데 진짜 요 근래 몇 달은 이 말이 정말 실감 나는 것 같아요. 어떻게 지나가는지도 모르게 지나가버렸습니다. 아까운 내 시간ㅠㅠ

 

물론 그렇다고 계속 놀기만 한 것은 아닙니다. 프로젝트 커밋도 정말정말 열심히 했습니다.

9월부터 11월 커밋!!

물론 저 혼자만 열심히 한 것은 아니고, 같이 프로젝트를 하는 분들의 열렬한 노력(...?)과 함께 개발하고 있습니다! 

사실 프로젝트 초기에는 "이걸 같이 할 수 있을까?" 라는 부분도 많았습니다. 저번에도 언급했던 여러 리소스 적인 부분도 그렇고, 특히 개발 기획이나 설계에 관한 부분도 그랬습니다.

 

사실 혼자서 할 때는 설계나 기획이라고 할 만한 것 없이 그냥 혼자서 대충 머릿속으로 그리고, 그걸 코드로 만들고, 테스트하고 수정하고를 반복하면서 했다면 이제는 그렇게 하기에는 어렵다는 것을 알았습니다. 개발자들이 각자 동시에 기능을 개발할 때 서로 영향을 주는 부분을 체크해야 하는 부분도 있었고, 특히 퍼블리셔분이 함께하기에 이전처럼 "이렇게 요렇게 개발하고 여기에 대충 붙이자"가 아닌 어디에 버튼위치를 잡고, 거기에 어떤 기능이 들어가는지 모두 의논하는 과정이 필요했습니다.

 

이렇듯 처음에는 굉장히 삐걱거렸었는데 2개월쯤 지나니 생각보다 "생각보다 시너지가 있는데?" 라는 생각이 들 정도로 많은 부분들이 정리되고 만들어졌습니다. 퍼블리셔분은 프로젝트의 구멍 난 부분들을 너무 이쁘게 메워주셨고, 결과 메인 채팅룸 대시보드와 로그인 페이지 모두 엄청나게 바뀌었습니다. 다른 두 개발자분께도 많은 도움을 받았는데 특히 기능 개발에 앞서 "어떻게와 왜"라는 부분에 대해서 많은 걸 배울 수 있었습니다. 예컨대 제가 "이거 개발해 주세요 이렇게 바꿔주세요"라고 할 때  단순히 "그래요 요렇게 요렇게 개발할게요"가 아닌 "기능이 왜 추가되어야 해요? 어떤 목적이 있어요? 어떻게 바꿔야 해요? 다른 방법이 없어요? 하기 싫은데요? 더 쉬운 거로 개발할게요"라고 말하며 저를 멈춰 세우곤 했습니다. 그 덕에 "이게 정말 필요한 부분일까? 지금 당장만 좋은 기능일까 혹은 앞으로도 이용할 수 있는 기능일까? 또 앞으로 추가적으로 확장하기 위해서 지금부터 고려해야 하는 사항은 무엇일까?"라는 고민을 해보게 되었습니다.

 

이렇게 시너지가 커지는것에 맞춰 아쉬운 부분도 커져갔는데 바로 제대로 된 기획과 디자이너 분이 없다는 것입니다. 매번 "퍼블리셔분께 이거 만들어주세요ㅠㅠ"라고 요청하면서도 제대로 된 디자인이 없으니 "디자인은 맘대로 해주세요..."라고 할 수밖에 없었거든요. 처음에는 그냥 해주시면 좋겠다라고 하면서도 생각해 보면 개발자에게 "이런 기능이 필요해요. 그런데 기획은 맘대로 해주세요"라고 말하는 거랑 마찬가지라는 이야기를 어디선가 들었고, 그때부터 내가 생각보다 무리한 부탁을 했구나 라는 생각이 들어서 굉장히 조심하게 되었습니다.

 

여하튼 잡설은 여기 까지만 하고 이제 메인으로 넘어가겠습니다!


2. 소셜 로그인 with Firebase

이번 개발의 메인이자 핵심이며, 알파이자 오메가인 소셜 로그인 기능이다. 소셜 로그인은 firebase를 통해서 로그인하는 방식으로 개발했다. 처음에는 firebase는 전혀 생각하지 않았고, 회원가입 페이지를 만들고, 이메일 인증 수단도 만들고,  언제나처럼 DB에 user 테이블을 만들고 넣는 형식으로 생각했었다.

그러다가 회사에서 선임분이 "이번 기회에 firebase 를 해보는 게 어떨까?"라는 이야기를 주셨고, 그때부터 firebase를 알아보았다. firebase는 생각보다 재미있는 기술이었다. Firebase를 통해 로그인하면 Firebase Auth의 인증 기능을 그대로 사용할 수 있고, 특히 유저 이름·이메일 등 민감정보를 Firebase가 대신 관리해 주기 때문에 social_user 테이블을 최소한의 구조로 단순화해 개발할 수 있었다. 또한 Firebase Auth는 access token과 refresh token에 해당하는 인증 토큰도 자동으로 발급해 주고, 우리 서버는 그 토큰을 Firebase에 검증 요청만 하면 되기 때문에 내 입장에서는 거의 혁명에 가까울 만큼 편리했다.

FireBase의 장점

1. 백엔드 없이 빠르게 개발 가능
- 서버 구축·배포 없이 인증, DB, 스토리지 등을 바로 사용 가능 → 개발 속도 매우 빠름.

2. 강력한 인증(Authentication) 기능
- 소셜 로그인·이메일 로그인·토큰 관리 등을 Firebase가 전부 처리 → 보안 걱정 ↓.

3. 실시간 기능 제공
- Firestore/Realtime DB로 실시간 데이터 동기화 구현이 쉬움 → 채팅/상태 표시 등에 유리.

4. 운영 부담 거의 없음
- 스케일링·보안 패치·서버 관리 자동 → DevOps 부담 크게 줄어듦.

5. Google Cloud 연동 쉬움
- 확장 필요 시 Functions, BigQuery 등과 자연스럽게 연결 가능.

6. 배포가 간단
- firebase deploy 한 번으로 프로젝트 배포 + 기본 CDN 제공.

 


3. Firebase 로 소셜 로그인 개발하기 - k8s에서 어떻게?

사실 내가 본격적으 개발한 부분인 QR 로그인은 소셜 로그인은 이미 구현된 이후였기에 실제로 Firebase 개발하면서 어떤 어려움이 있었는지는 정확하게 알지는 못한다. 다만 함께 하는 개발자분이 firebase 관련된 js와 sdk를 붙이기 시작할 때 나는 나대로 "firebase 관련된 json 파일을 어떻게 github에 올리지 않고 k8s에 적용할까?"에 대해서 고민했다. firebase sdk를 처음 init 할 때는 어떤 소셜 로그인과 연동하는지에 따라서 관련된 json 파일이 필요하다. 예를 들어 우리는 google 이기에 google 관련된 json 이 생성되었고, 이걸 firebaes init 시 읽을 수 있도록 설정해 줘야지만 제대로 firebase 연동이 가능했다.

 

문제는 로컬에서는 로컬 디렉토리에서 해당 파일을 가져와 읽어야 했고, k8s를 사용할 때는 k8s에 맞게 가져와서 읽어야 한다는 점이었다. 사실 로컬에서 가져와서 읽을 방법이야 많고 많았으니, 결국 k8s에서 어떻게 대응할 것인가? 만 고민하면 되는 문제였다. 이렇게 저렇게 찾아보다가 문득 application.properties 랑 똑같이 configmap 혹은 secret에 넣어두고 쓰면 어떨까?라는 생각을 하게 되었다.

즉, 로컬에서는 로컬에 맞는 디렉토리에서 파일을 가져와서 읽고, k8s에서는 secret에 저장해 둔 뒤 컨테이너가 시작될 때 환경변수로 넣어주는 방식인 것이다. 

 

FirebaseConfig

@Component
@Slf4j
public class FirebaseConfig {

    @PostConstruct
    public void initializeFirebase() throws IOException {
        GoogleCredentials credentials;

        // 1단계: GOOGLE_APPLICATION_CREDENTIALS 환경변수 확인
        String credentialsPath = System.getenv("GOOGLE_APPLICATION_CREDENTIALS");

        if (credentialsPath != null && !credentialsPath.isEmpty()) {
            // 환경변수가 있으면 해당 경로의 파일 사용
            FileInputStream serviceAccount = new FileInputStream(credentialsPath);
            credentials = GoogleCredentials.fromStream(serviceAccount);
            log.info("Firebase 초기화: GOOGLE_APPLICATION_CREDENTIALS 사용");
        } else {
            // 환경변수가 없으면 기본 경로에서 JSON 파일 찾기
            credentials = loadCredentialsFromDefaultPath();
            log.info("Firebase 초기화: 기본 JSON 파일 사용");
        }

        FirebaseOptions options = FirebaseOptions.builder()
                .setCredentials(credentials)
                .build();

        FirebaseApp.initializeApp(options);
    }

    private GoogleCredentials loadCredentialsFromDefaultPath() throws IOException {
        ClassPathResource resource = new ClassPathResource("firebase/google_account_key.json");

        if (resource.exists()) {
            InputStream inputStream = resource.getInputStream();
            GoogleCredentials credentials = GoogleCredentials.fromStream(inputStream);
            return credentials;
        } else {

            // 여러 위치에서 JSON 파일 찾기
            String[] possiblePaths = {
                    "/etc/firebase/google_account_key.json",  // K8s 마운트 경로일
                    //"firebase-service-account.json"  // 현재 디렉토리
            };

            for (String path : possiblePaths) {
                try {
                    File file = new File(path);
                    if (file.exists()) {
                        log.info("Firebase JSON 파일 발견: " + path);
                        return GoogleCredentials.fromStream(new FileInputStream(file));
                    }
                } catch (IOException e) {
                    log.info("파일 읽기 실패: " + path);
                }
            }

            throw new RuntimeException("Firebase 설정 파일을 찾을 수 없습니다.");
        }
    }
}

 

 

K8S 에서는 이렇게 세팅!

k8s 에는 이렇게 세팅!

 

chatforyou deployment에서는 이렇게 세팅!

      volumes:
        - name: config-volume
          configMap:
            name: chatforyou-config
            defaultMode: 420
        - name: tls-cert
          secret:
            secretName: chatforyou-tls
            defaultMode: 420
        - name: nginx-config
          configMap:
            name: nginx-backend-config
            defaultMode: 420
        - name: firebase-config <--- secret 세팅
          secret:
            secretName: firebase-secret
            defaultMode: 420
      containers:
        - name: chatforyou-container
          image: ghcr.io/sejonj/chatforyou:20251103222957
          ports:
            - containerPort: 8443
              protocol: TCP
          resources: {}
          volumeMounts:
            - name: config-volume
              mountPath: /config/application.properties
              subPath: application.properties
            - name: tls-cert
              mountPath: /etc/tls
            - name: firebase-config <--- 여기에서 mount
              readOnly: true
              mountPath: /etc/firebase

4. QR 로그인 개발하기 - Electron 에서 소셜로그인은 어떻게...?

사실 chatforyou 가 웹 환경에서만 동작하면 된다면 QR 로그인이 굳이 필요하지는 않았을 것이다. 왜냐하면 웹에서는 소셜로그인 버튼을 클릭하고, 구글로 연결되고 연동돼서 로그인되기 때문이다. 그런데 우리 Chatforyou 무려 데스크톱 앱을 지원하는 엄청난! 프로젝트이기 때문에 QR 로그인이 꼭 필요했다.

그러나 이 소셜로그인이 예상치 못한 문제를 발생시켰다. Firebase 소셜로그인은 기본적으로 redirect / popup 방식(OAuth)이 필요한데 Electron 으로 개발된 데스크톱 앱은 일반 웹 브라우저가 아니라서 정상적으로 리다이렉트가 되지 않는다는 엄청난 문제가 있었다. 만약 인증에 성공한다고 해도 Firebase Auth는 로그인 성공 후 특정 URL로 콜백 되는데 Electron 앱에는 “브라우저 URL”이라는 개념이 아닌 file://... 같은 경로를 사용하기 때문에 제대로 된 콜백은 받을 수 없다.

 

솔직히 이 문제를 접하고 나서 "데스크톱 앱 기능을 포기할까?" 라는 생각도 들었다. 그 정도로 소셜 로그인 기능을 간절했고, 반대로 데스크톱 앱에서 도저히 어떻게 해결할지 방법이 보이지 않았기 때문이다. 그렇게 해결 방법을 찾지 못하고 개발도 손을 놓은 채 한 주, 두 주 흘러가다가 친구들과 게임을 하는 날이었다. 우연찮게도 그날은 chatforyou 서버 이슈로 인해 다급하게 디스코드로 옮기게 되었다. 그렇게  바탕화면에 깔린 디스코드를 실행하고 자연스럽게 QR 로그인을 하면서 디스코드 로그인을 할 때였다. 문득 하나의 번뜩이는 아이디어가 스쳐 지나갔다.  '똑같이 electron으로 개발된 디스코드가 QR 로그인을 통해 이렇게 쉽게 로그인을 할 수 있다면 우리도 QR 로그인을 개발하면 데스크톱 앱에서 로그인되는 거 아닌가?' 하는 생각이 들었던 것이다.

 


5. QR 로그인 - 개발방향과 설계

  • QR 로그인은 실제 코드양이 많은 관계로 자세한 코드는 git repo를 참고해 주시면 감사하겠습니다!

QR 로그인이라는 아이디어를 통해 ‘어떻게 해결할까?’를 해결하고나니 이후로 QR 로그인 개발 방향과 설계가 금방 명확해졌다. 

  • 개발 방향 아래 4단계로 정해졌다.
    1. QR 코드를 활용해 모바일에서 인증하도록 유도
    2. Firebase Authentication 으로 유저 인증 처리
    3. Desktop 앱은 직접 OAuth 처리 없이 Firebase에서 제공하는 토큰으로 인증 확인
    4. Redis를 활용해 QR 로그인 세션 관리 및 상태 동기화
  • 이때 개발 시 핵심전략은 아래 3가지를 메인으로 하였다!
    • polling 방식 로그인 체크 : 로그인 성공 여부를 3초 간격으로 체크
    • QR 로그인 세션 유효시간 설정 : 5분
    • 로그인 완료 후처리 : 인증 완료 후 Desktop 앱에 로그인 정보 전달

다음은 어떻게 개발할까에 대한 설계였는데 처음에는 web과 desktop 앱을 분리해서 설계를 했었는데, 생각해 보니 굳이 web과 Desktop을 구분해야 할 필요는 없었기에 따로 구분하지 않는 방향으로 진행했다.

1) ChatForYou(Web && Desktop App)

  • QR 로그인 버튼 클릭 → QR 팝업 생성
  • 3초마다 서버에 상태 조회 API 호출
  • 인증 완료 확인 시 로그인 처리

2) Mobile

  • QR 코드 스캔 → 구글 로그인 페이지로 이동
  • 구글 계정 선택 → Firebase 인증 수행
  • 인증 완료 후 “이 창을 닫아도 됩니다” 표시

3) Server / Backend

  • QR 로그인 세션 정보 저장: sessionId, QR URL, 상태(PENDING), 생성 시간 등 → Redis
  • Firebase를 통해 유저 인증
  • 인증 완료 시 Redis의 QR 세션 상태를 AUTHENTICATED 로 업데이트
  • Desktop 앱의 상태 조회 요청에 인증 완료 정보 반환

4) Redis

  • QR 세션 상태 및 관련 데이터 저장
  • 3초마다 Desktop 앱 요청 처리 시 빠른 조회

사용자 QR 로그인 처리 Flow

flowchart TD
    A[Desktop App: QR 로그인 버튼 클릭] --> B[QR 팝업 생성 및 QR 코드 표시]
    B --> C[사용자: 모바일로 QR 코드 스캔]
    C --> D[모바일 브라우저: 구글 로그인 페이지 이동]
    D --> E[사용자: 구글 계정 선택 및 로그인]
    E --> F[Firebase Auth: 인증 성공]
    F --> G[서버 콜백: "이 창을 닫아도 됩니다" 메시지 표시]
    G --> H[서버: Redis QR 세션 상태를 AUTHENTICATED 로 업데이트]

    B -. 3초마다 상태 조회 .-> I[Desktop App: 백엔드에 상태 조회 API 요청]
    I --> J[백엔드: Redis에서 QR 세션 상태 확인]
    J -->|미인증| I
    J -->|AUTHENTICATED| K[백엔드: 로그인 정보 응답]
    K --> L[Desktop App: 로그인 처리 완료]

 

 

5. 소셜 로그인 && QR 로그인 확인하기

실제로 로그인은 어떻게 구현되었을까? 아래는 실제 로그인 페이지이다.

로그인 페이지!

여기서 QR 로그인을 누르면...?

요렇게 깔끔한 QR 로그인 창이 등장한다

로그인 후에는 단순히 로그인 페이지에 남아있는 것이 아닌 메인 대시보드로 이동하게 된다. 추가로 지금까지는 방을 만들거나 입장할 때 등 로그인 기능이 따로 없었는데 앞으로는 반드시 로그인해야지만 사용 가능하도록 권한체크도 함께 추가해 두었다.

 

 

오오 QR 오오

 


6. 마무리하면서 - 다음 개발 예정 기능은...?

이제 소셜로그인도 되었고, 자연스럽게 다음 기능으로 넘어가서 어떤 기능을 추가하면 재미있을까? 에 중점을 두고 기획 중에 있습니다. 매번 다른 분들께 강조하는 게 '각자 본인이 재미있어 보이는 기능 찾아오시면 그거 넣으시면 됩니다'라서 나도 마찬가지로 어떤 게 재미있을지 찾다가 이제야 비로소 "녹화" 기능을 본격적으로 보기 시작했다.

 

당연하다면 당연하게도 기능은 구현 난이도는 지금까지 내가 했던 모든 기능들을 포함해서도 가장 어려운 게 아닐까...라는 생각이 들 정도이다. 단순히 "녹화"만 넣는 게 아닌 녹화 권한 설정은 어떻게 할 것인지, 어떤 확장자로 저장할지, 최대 녹화 시간은 몇 분인지, 녹화 파일 다운로드 가능시 어떻게 알림을 줄지, 어디에 저장할지 언제 삭제할지 등등 모든 부분에 대해서 고민해야 했기 때문이다. 1차 목표는 가능한 간단하게 개발한 후 점차 개선해나갈 생각이다. 추가로 언젠가 기회가 된다면 녹화를 요약하는 기능도 넣어보면 어떨까 한다.

 

아래 영상은 테스트 녹화본으로 생각보다 음질, 버퍼 등 아직 문제가 많은 모습을 보이고 있다. 그래도 아직은 부족한 게 많은 부끄러운 기능이지만 한편으로는 녹화가 되고, 파일도 나온다는 점에서 충분히 가치 있는 영상이라고 생각한다. 

아직은...많이 부족하다

 

벌써 2025년이 지나가고 있다. 벌써 다음 달이면 개발자 나부랭이가 된 지 3년이 다가오고 있다. 사실 연차만 늘어나고 이게 발전하고 있는 게 맞나...?라는 생각이 문득문득 나를 스쳐 지나간다. '내가 재미있어하는 걸 개발' 하는 것과 '내 발전을 위해 하는 개발' 하는 것 중 어떤 게 더 나에게 맞는지 혹은 나에게 좋은지는 모르겠지만... 뭐, 아직은 계속할 수밖에 없겠지.


Reference

https://haranglog.tistory.com/25

 

🤔 Firebase API Key를 공개하는 것이 안전합니까?

📌 고민하게 된 계기 스터디 모집 개인 프로젝트를 진행하면서 백앤드로 firebase를 사용하게 되었습니다. 사용하면서 API Key이니까 당연히 .env 파일로 관리해야지라고 생각한 뒤 관리를 하게 되

haranglog.tistory.com

 

https://seopseop911.tistory.com/37

 

안드로이드 - QR코드 스캔하여 Firebase Realtime Database에 저장하기 (QR스캐너)

프로젝트 중 QR코드를 스캔하는 기능이 필요해 구글링 해보던 중 QR 코드 스캔을 할 수 있는 라이브러리가 있다는 것 을 알고 블로그를 참고 하였다. firebase의 데이터베이스와 연동하는 것은 지난

seopseop911.tistory.com

https://velog.io/@c-on/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-firebase-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

🔥 자바스크립트로 firebase 사용하기

파이어베이스는 구글의 NoSQL중 MongoDB와 함께 문서형종류에 해당하는 데이터베이스이다.key-value \+ 가장 기본적이지만 범위검색이 힘듬column \+ 키-값에서 확자외어 한 개의 컬럼에 여러개를 저장

velog.io

https://arcane222.tistory.com/34

 

WebRTC (6) - Kurento RecorderEndpoint (2)

1) RecorderEndpoint callback 비디오 녹화를 위해 RecorderEndpoint 생성 시 이벤트 리스너 콜백 함수를 등록 해줄 수 있다. addRecordingListener - 녹화가 시작되면 호출되는 이벤트 리스너 등록 addPausedListener - 녹

arcane222.tistory.com

 

댓글