22.08.03 TIL

옵주비·2022년 8월 4일
1
post-thumbnail

나만의 무기

오늘도 폭풍같은 하루를 보냈다.

엄청난 오늘의 커밋 리스트. 아침 일찍부터 나와 작업하고, 오후에 폭풍 커밋을 했다.

오전에 팀 회의를 통해 개발을 오늘 마감하기로 했기에 좀 더 박차를 가했다. 남은 이틀은 혹시나 모를 버그 발생 시에 디버깅을 하는 것과 발표 준비에 집중하자는 의견이 많았기 때문이다. 아까 회의를 할 때는 개인적으론 하루만 더 다같이 개발을 하고 싶다는 생각을 했는데, 이 글을 쓰는 지금은 완전히 생각이 바뀌었다. 완성도를 높이는 과정에서 갑자기 새로 접하는 버그가 2~3개 발생했는데, 이걸 내일이나 모레 접했다면..... 생각만 해도 끔찍하다. 3일 전인데도 순간 정신이 혼미했다.

나만의 무기 만들기 과정을 시작할 때 작성한 글에서도 언급했지만 원래 발표를 하고 싶었는데, 우리 조뿐만 아니라 우리 반은 모두 팀장들이 발표를 맡았다. 아쉽지만 팀장을 하지 않은 것도 나의 선택이니 발표를 하지 못하는 것은 어쩔 수 없는 부분인거 같다. 대신에 내가 최선을 다할 수 있는, 팀에 가장 기여할 수 있는 부분은 개발 역량인거 같아서 내일도 나는 약간의 개발을 할 것 같다.

발표 준비에 이미 3명 이상이 붙어있기에 나까지 붙으면 투머치인거 같기도 하고, 내가 워낙 디테일을 중시하는 타입이라... 괜히 막판에 서로 감정 상하지 않게 깔끔하게 빠지는게 나을거 같았다. 소프트웨어 엔지니어가 되기위해 정글에 왔는데 굳이 경영학과 때 수없이 했던 발표준비 PPT준비 안하고 개발에만 집중할 수 있어서 오히려 좋은거 같기도 했고! 하긴 정글에 와서 발표는 종종 했는데 0주차를 제외하곤 매번 즉석에서 했다.

발표를 하지 못하는 아쉬움은 포스터 세션에서 맘껏 날릴 예정이다 🙃 오늘 작업한 내역 중에 가장 메인은 QR로 하는 명함교환 부분과 실시간으로 메세지 읽음여부를 나타내는 것이었다. 메세지의 경우 어제 언급한 '내가 읽지 않은 횟수'를 처리해준 뱃지와 비슷하게 '상대가 읽지 않은 횟수'를 가지고 처리를 하는 부분이라, QR 관련한 부분에 대해 다뤄보도록 하겠다.

QR 오류 해결

어제 구현한 QR 관련하여 발생한 2가지 문제점을 해결했다.

카메라 자동으로 켜주기

우선, 어제 언급했던 카메라 문제이다. 카메라가 자동으로 켜지지 않고, 화면을 터치해야만 켜지는 증상이 있었다. 패키지에 있던 예시 코드를 천천히 살펴보니, RESUME 버튼이 있어서 그 코드를 가지고 한번 실행해보았다. 처음에 카메라가 OFF더라도 그 버튼만 눌러주면 알아서 카메라가 실행되길래, 그 안에 있는 코드를 가지고 해결해보려고 했다.

Container(
	margin: const EdgeInsets.all(8),
	child: ElevatedButton(
		onPressed: () async {
			await controller?.resumeCamera();
		},
		child: const Text('resume', style: TextStyle(fontSize: 20)),
	),
)

위의 내용을 반영해 dart파일이 실행될 때, 가장 먼저 실행되는 코드인 initState내에 controller?.resumeCamera()를 넣어주었다. 결과는....? 놀랍게도 해결되지 않았습니다.

그래서 카메라가 OFF라면 켜주기 등등 온갖 시도를 해봤지만, 진전이 없었다. 처음에 켜지면 무조건 그 다음에는 켜지지 않았다. 여기서 '다음'이란, 촬영모드로 들어가 카메라가 켜진 후에 뒤로가기를 해서 다시 최초 교환화면으로 간 후에, 다시 촬영모드로 들어간 상태를 뜻한다. 두 사용자가 우연의 일치로 둘 다 카메라를 켰다가 당황해서 둘 다 뒤로가기를 한 후에, 역할 분배를 한다면? 그럼 1명의 사용자는 2번 연속으로 촬영 모드에 들어가게 될 것이다. 이런 상황에 카메라가 갑자기 나오지 않는다고 생각하니 난감했다.

'화면을 터치하면 카메라가 실행됩니다' 라고 문구를 적어주자니, 자존심이 상했다. 그래서 생각해낸 묘수가 바로 Future를 활용하는 것이었다. 다트에서 Future를 활용하면 일정시간이 경과한 미래에 값을 할당하거나 함수를 실행할 수 있다. 따라서 이번엔 아래와 같이 Future를 활용해 resumeCamera()를 하도록 시도해보았다.

Future.delayed(Duration(milliseconds: 600), () async {
      await controller?.resumeCamera();
    });

이렇게 0.6초 후에 resumeCamera()를 실행하도록 해주었더니, 잘 해결되었다. 1초나 0.8초는 설정해보니 체감상 약간 느리고 같아서, 0.6초로 설정해주었다.

QR을 통해 socket방 입장시켜주기

또한, 명함을 받으려는 사람이 상대방의 QR을 인식했을 때 교환으로 이어지지 않는 오류도 해결했다. 코드를 뜯어보니, 저번에 메세지 오류를 해결했을 때와 비슷한 문제였다. String과 int ... 저번과는 거꾸로, 이번에는 서버에서int로 보내주는데 그 값을 int.parse() 하려고 하니까 에러가 났다. int.parse()의 대상은 String이어야 하기 때문이다. 원인을 파악하고나니 해결은 어렵지 않았다.

QR코드 암호화

에러를 잡는김에, 아예 더 완성도를 높여주었다. 더 복잡한 이름의 소켓방을 활용해서 암호화를 시켜주는 것이다. 원래는 sender의 아이디값만 가지고 소켓방 이름을 지었는데, 그 앞에 키워드를 붙여주었다. 키워드는 보안상 비밀 !

encoding (by Sender)

'${keyword}${Senderid}' 이름을 가진 이 소켓방 이름을 암호화 해주었다.

Codec<String, String> stringToBase64 = utf8.fuse(base64);

....

body: widget.isSender
	? Center(
		child: QrImage(
		data: stringToBase64.encode('${keyword}${widget.myId.toString()}'),
        backgroundColor: Colors.white,
        size: 200,
        ))      
    : Column(
    	....
        촬영모드
        ....
      )
.....

base64는 이진 데이터(바이트 배열)를 아스키코드로 표현해내는 인코딩 방식 중 하나인데, 텍스트뿐만 아니라 이미지 등을 저장할때도 사용한다. 참고로 아스키코드는 7 bit encoding으로 0부터 127까지의 숫자를 사용하고, 나머지 1비트는 이 정보가 유효한지를 나타내는 parity bit로 사용한다. 그런데 이 1비트를 처리하는 방식이 시스템별로 상이하기에, 그를 감안하더라도 안전한 64개의 아스키코드만 사용하는 인코딩 방식이 base64이다.

앞서 언급했듯이 base64로 암호화하기 위해선 바이트 배열이 필요하기에, 우리 프로젝트를 위해 사용하려면 우선 utf-8 인코딩으로 String을 바이트 배열로 변환해주어야 한다. 결국 작업이 2번(utf8로 암호화 + base64로 암호화) 필요하다. 이 2가지 작업을 하나로 결합해서 실행할 수 있도록 하기 위해서 dart:convert 라는 기본 패키지가 제공하는 fuse를 사용하였다.

decoding (by Receiver)

	setState(() {
		result = scanData;
        if (result != null) {
          try {
            String? chatRoomId = stringToBase64.decode(
                result!.code!);
            if (chatRoomId.substring(0, 14) == keyword) {
              String? senderIdinStr = chatRoomId.substring(14);
              senderId = int.tryParse(senderIdinStr);
              if (senderId != null) {
                if (senderId! > 0) {
                  socket.emit('join', chatRoomId);
                  socket.emit('took', {
                    'chatroomID': chatRoomId,
                    'senderID': senderId,
                    'receiverID': widget.myId
                  });
                  setState(() {
                    isValid = true;
                  });
                }
              }
            }
            if (!isValid) {
              showSnackbar(errorMsg);
            }
          } catch (e) {
            debugPrint('유효하지 않은 바코드입니다.');
            showSnackbar(errorMsg);
          }
        }
      });
      
......

암호화한 QR을 읽어들이는건 위와 같은 작업을 통해 이루어진다. 한번이라도 에러가 나거나, 조건문을 통과하지 못하면 소켓방에 입장할 수 없으며 isValid=true 까지 도달할 수 없다. 최초에 isValid를 false로 초기화 해놓았기에 우리 NEMO 앱에서 발행한 것이 아닌, 즉 유효하지 않은 QR을 찍어도 소켓방으로 넘어가지 않는다.

참고로 int.tryParse 후에 null도 확인하고 > 0도 확인하는 것은, 혹시라도 암호화된 값이 '{keyword}0' 형태일까봐 안전장치로써 적용해놓았다. 확인해본 결과 int.tryParse('0')의 결과가 null이 아니라 0으로 나오기 때문이다. 유효한 SenderId는 자연수여야 하기 때문에, 만약의 사태를 대비해 한번 더 처리했다 😁

0개의 댓글