첫번째 프로젝트 : 커뮤니티 서비스 in Flutter Webview
회사 첫번째 프로젝트 커뮤니티 서비스
배경 🌁
스타트업에 입사하고 처음 맡게된 프로젝트는 커뮤니티 게시판이었다.하지만 일반적인 커뮤니티 게시판과 달랐던 점은 단순 웹사이트가 아닌 플러터를 사용해서 만든 앱에 들어가는 웹뷰를 만들어야 했다.
개발자가 되고 처음으로 맡은 업무였고 프론트쪽은 주도적으로 혼자 담당해야 했기 때문에 긴장이 됬고, 문제없이 잘 만들어 나 자신을 어느정도 증명해야겠다는 생각에 열의가 넘쳤던 프로젝트였다.
프로젝트 기간
2022.02.01 ~ 2022.02.28 (테스트 및 배포까지)
기술 스택 ⚙️
- Javascript
- React.js
- Styled components
- Redux toolkit
- Axios
- Gitlab CI-CD
기술 선택 이유
기술에 대한 고민을 하지 않았다. 심지어 회사 내에 프론트엔드 개발 하시던 분이 없었어서 딱히 정해진 기술이 없었다. 즉 내가 최근에 배운 기술들을 사용하여 프로젝트를 진행 하였다.
총 페이지 갯수 : 5개
- 메인 게시물 리스트 페이지
- 게시물 상세 페이지
- 내 게시글 및 댓글 관리 페이지
- 게시물 작성 페이지
- 게시물 수정 페이지
주요 기능들
- 무한스크롤이 되는 게시물 리스트 페이지
- 게시물 작성, 수정, 삭제
- 대댓글 기능
- 사진 업로드
- 검색 기능
- 유저 및 게시글 차단
- 내 게시글, 댓글관리 페이지에서 해당 게시글이 있는 페이지로 이동 (포커스 까지)
- 플러터와의 연결 (imagePicker, 카메라, SnackBar )
- 기타 짜잘한 UX 기능들
웹뷰 사전 지식
웹뷰란 간단하게 말해서 앱내에서 사용가능한 인터넷 브라우저이다. 앱 보다 개발이 조금 더 용이 하다는 관점에서 많은 회사들이 모바일의 많은 화면들을 웹뷰로 만들고 있다. 간단한 동작원리는 예를들어 플러터와 JS간의 통로(채널)를 하나 만들고, 이 통로를 통해서 플러터에서 JS에 선언된 특정 함수들을 호출 할수 있게 된다. 이를 통해 브라우저이지만 모바일에서 볼수 있는 기능들을 사용 수 있게 된다. 이 통로들을 통해서 플러터 <-> JS간의 데이터도 서로 전달 할 수 있다. 👉🏻 플러터와 웹뷰 간의 상세한 동작 원리 및 예시는 추후에 정리 할 예정.
웹뷰를 사용하는 이유
- 네이티브 앱에 비해서 상대적으로 개발이 어렵지 않다.(웹 쪽이 레퍼런스도 더 많다.)
- 앱은 배포를 한후 심사를 받는 과정까지 시간이 소요되지만(거절 당할 수도 있음), 웹은 배포를 하게 되면 즉사 반영된다. -> 생산성이 훨씬 뛰어나다.
- 앱보다 웹이 개발자 풀도 많다.
어려웠던 부분 🥶
1. 플러터 <-> JS 양방향 통신
통신이라고 표현한 이유는 플러터와 JS가 데이터를 전달하고 받을 수 있었기 때문이다.
기존에는 웹브라우저 환경에서만 개발을 했기 때문에, 전혀 문제가 없었지만… 이번에는 플러터로 만든 앱 내부에 띄워야할 ‘웹뷰’였기 때문에 해당 화면에서 네이티브 기능을 사용하려면 추가로 작업해줘야 하는 부분이 있었다.
추가로 네이티브 기능들을 호출하는 함수들을 웹 브라우저 개발 환경에서 호출하게 되면 에러가 발생한다. (예를들어 앱 내의 화면을 여는 버튼을 눌렀을때)
당연히 에러가 발생한다. 자바스크립트 코드에는 따로 callHandler라는 함수를 선언하지 않았기 때문.
해결 🤔?
애초에 웹 브라우저에는 이런 네이티브 기능을 제공하는 함수가 존재하지 않는다.
플러터에서 선언한 함수들을 JS에서는 특정 flag와 함께 호출하게 코드를 작성해 놓는다. 웹뷰 코드들이 플러터 안에 임베디드 되면 플러터가 이 코드를 읽고 해당 함수를 특정 시점에 실행시킬 수 있다.
👉🏻 플러터에서 살행 시킬 함수들을 JS내에서 콜백함수로 선언해 놓음
// 2. Image Picker
export const ImagePicker = async (type, count) => {
const response = await window.flutter_inappwebview.callHandler(
"ImagePicker",
type,
count
);
return response;
};
// 3. Toast Message
export const Toaster = async (msg) => {
const response = await window.flutter_inappwebview.callHandler(
"Toaster", // key
msg // 출력할 메시지
);
};
// 4. Sneak Bar
export const SnackBar = async (msg) => {
const response = await window.flutter_inappwebview.callHandler(
"SnackBar", // key
msg // 출력할 메시지
);
};
👉🏻 실제 특정 시점에 플러터에서 호출할 수 있도록
// 1. ImagePicker 사용
useEffect(() => {
(async () => {
await ImagePicker('CLEAR', 0);
})();
}, [finalModifiedImageList]);
// 2. SnackBar 사용
try {
const response = await userService.blockUser(blockUserId);
onClose();
// 호출만 하면 됨
SnackBar('해당 유저의 게시글 및 댓글을 차단했습니다');
}
2. 플러터 <-> 서버 통신
기존 웹에서만 개발을 진행할때는 Axios라는 라이브러리를 사용하여 서버와 통신을 했다. 현재 서비스에서 서버와 통신을 하려면 인증과 관련된 토큰이 필요한데, 웹뷰에서 인증 서버에 요청하여 받은 토큰이 아닌, 플러터에서 인증 서버를 통해 받아온 토큰을 사용해야 했다. 즉 웹뷰에서 발생하는 모든 요청, 응답은 Axios가 아닌 플러터의 fetching 라이브러리중 하나인 DIO를 사용하여 통신을 진행하게 만들었다.
해결 🤔?
자바스크립트 코드에서 DIO라는 인스턴스를 만들었다.(실제 DIO는 아님) DIO 인스턴스의 역할은 플러터가 웹뷰내에서 DIO 인스턴스 코드를 읽고 API 호출할 시점에 DIO라이브러리를 사용해 서버와 통신하게 한다.
추가로 실제 개발할때는 웹 브라우저에서 Axios를 사용한 통신을 해야하기 때문에, web환경과 app환경을 애초에 분리시켜, web환경에서는 Axios를 사용한 통신, production 웹뷰 환경에서는 플러터의 DIO를 사용한 통신으로 나눠서 개발하였다.
환경변수에 현재 플랫폼(web or app)을 미리 담아놓고 npm run webstart
npm 커스텀 명령어를 사용하여 webstart일때는 Axios를 사용한 웹브라우저 개발 환경으로 셋팅을 해놓았다.
👉🏻 DIO 인스턴스 만들기
import { DioResponse } from "./DioResponse";
import { DioErrorResponse } from "./DioErrorResponse";
export function DioInstance() {
const FAIL_CLIENT = "FAIL_CLIENT";
const FAIL_SERVER = "FAIL_SERVER";
const SUCCESS = "SUCCESS";
const REDIRECT = "REDIRECT";
const UNKNOWN = "UNKNOWN";
function resolveStatus(statusCode = 0) {
if (Number.isNaN(statusCode)) return UNKNOWN;
if (parseInt(statusCode / 100, 10) === 2) return SUCCESS;
if (parseInt(statusCode / 100, 10) === 3) return REDIRECT;
if (parseInt(statusCode / 100, 10) === 4) return FAIL_CLIENT;
if (parseInt(statusCode / 100, 10) === 5) return FAIL_SERVER;
return UNKNOWN;
}
// 🔫 이 부분이 실제 Flutter에서 실행하는 부분
async function getDioUtil(method, url, body = null) {
const response = await window.flutter_inappwebview.callHandler(
"DioUtil", // key
method,
url,
body
);
const statusText = resolveStatus(response.statusCode);
if (statusText === SUCCESS || statusText === REDIRECT) {
return new DioResponse(response.statusCode, response.data);
}
if (statusText === UNKNOWN) {
throw new Error("Dio: 알 수 없는 에러가 발생하였습니다.");
}
throw new DioErrorResponse(statusText, response.statusCode, response.data);
}
return {
get(url) {
return getDioUtil("GET", url);
},
post(url, body) {
return getDioUtil("POST", url, body);
},
put(url, body) {
return getDioUtil("PUT", url, body);
},
delete(url) {
return getDioUtil("DELETE", url);
},
};
}
👉🏻 DIO 인스턴스 사용
let api;
/* dio 플러터 인스턴스 연결 */
if (platform === 'mobile') {
api = new DioInstance();
} else {
/* Axios 인스턴스 설정 */
api = axios.create({
baseURL: process.env.REACT_APP_SERVER
// baseURL: 'http://192.168.10.175:8080'
});
//
/// 요청 (request)
api.interceptors.request.use(
...
);
/// 응답 (response)
api.interceptors.response.use(
...
)
}
3. 컴포넌트 설계
프로젝트를 만들다보니, 컴포넌트 설계에 고민을 안하고 만들었다 도중에 컴포넌트 구성을 바꾸거나 하는 일이 많았다. 요구 조건이 바뀌거나 추가될때마다, props의 갯수가 증가하였고 컴포넌트에서 받은 props가 증가하여 점점 컴포넌트 자체가 하는일이 많아지게 되었다.
배운 것
1. 웹과 앱의 양방향 통신
플러터가 Javascript 채널을 통해서 웹뷰내에서 자바스크립트를 컨트롤 할수 있다. 자바스크립트의 값을 받아올 수 도 있고, 값을 자바스크립트로 전달 할 수도 있었다. 이를통해 웹 브라우저에서는 사용할 수 없는 모바일 기능들과 플러터가 바로 서버와의 통신을 할 수 있었다. DIO 인스턴스를 만들어 봄으로써 HTTP 통신 부분에 대한 추상화도 할 수있게 되었다.
2. 서버에서 받아온 데이터의 흐름 파악
화면쪽 UI도 중요하지만 프론트엔드 개발자는 백엔드로 부터 받아온 데이터를 어떻게 컴포넌트간에 전달할지 고민을 해야한다. 이번 프로젝트를 통해 데이터를 최상단에서 요청후 응답받아 자식 컴포넌트에게 props로 전달하는 방식으로 데이터를 표현해 주었다.
3. 컴포넌트 설계
컴포넌트 단위로 설계하는 방식의 이점을 알게되었고, 그만큼 설계를 어떻게 할것인지 처음 공통단 개발은 어떻게 할 것인지에 대한 생각을 할 수 있게 되었다. 추가로 요구사항이 변경되거나 바뀌었을때 컴포넌트가 점점 비대해 지는 것을 경험했는데 이것을 어떻게 개선할 수 있는지 공부를 해야할 것 같다.
4. 컴포넌트 props에 스타일이 들어가도되는가?에대한 고민
공통으로 사용할 수 있는 컴포넌트를 만들면서, 스타일을 외부에서 넘겨받아 적용하는 컴포넌트들이 많았다.
<Input
bgColor="#F8F8F8"
width="100%"
padding="16px 48px 16px 16px"
fontSize="17px"
radius="14px"
placeholder="검색어를 입력해주세요"
fontColor="#2F3E39"
onChange={handleChange}
value={content}
onKeyPress={handleKeyPress}
/>
애초에 이런 방식이 맞지 않다고 생각이 들었다. props로 스타일을 넘김으로써 렌더링하는 jsx 부분의 코드도 너무 길어지고 가독성도 떨어졌다. 애초에 디자이너와 이야기해서 공통 컴포넌트를 만들어 styled-components내부에서 스타일을 지정하고, 혹시나 바뀌는 컴포넌트가 있다면 override하는 방식으로 스타일을 적용해야겠다고 생각했다.
5. Gitlab-CI-CD
처음 프로젝트를 만들고, 배포는 FileZila를 이용한 FTP방식으로 진행했다. 간편하다는 장점이 있지만 사람 손으로 하기때문에 언제든지 실수를 유발할 수 있다.(최신 파일이 아닌데 착각해서 올린다거나 등등). 이를 해결하기 위한 방법중 하나인 CI-CD로 빌드 후 배포를 자동으로 진행 시킬 수 있다. 백엔드 개발자 분께서 이 과정을 많이 도와주셨고, 간단한 CI-CD의 큰 흐름을 익힐 수 있었다.
느낀 점
간단하다고 생각했던 커뮤니티 게시판이었지만, 실제 프로덕트는 달랐다.
보여지는 것도 굉장히 중요하기 때문에, 디자이너분의 QA도 나름 정교하게 진행되었고, 생각보다 UX를 위한 디테일 기능들도 많이 추가가 되었다. 이로써 기존 토이프로젝트와 실제 프로덕트의 고민해야할 요소들이 훨씬 많음을 느겼다. 그래도 프로젝트 생성부터 시작해서 배포까지 한 사이클을 돌아보면서 내가 어떤것이 부족한지, 앞으로 어떤것을 더 공부해야할지가 어느정도 잡혔다.
추가로 혼자 작업하다보니, 일정을 산정하는 것이 어려웠다. 주니어 개발자이다보니 내가 하는 기능, 페이지가 어느정도 소요될지 예측하고 팀에게 전달하는 것 자체가 부담이고 힘들었다.
이 부분도 연습이 필요하고 앞으로 일을 하게되며 당연히 직면해야할 부분이기 때문에 일정관련 이슈도 항상 주의깊게 생각해야겠다.
마지막으로 제일 좋았던 점은 개발팀에서의 협업이라는게 어떤것인지 느꼈다. 백엔드 개발자와 API문서를 가지고 이야기하고 수정하는것, 플러터 개발자와 웹과 앱간의 통신에 대하여 맞춰보는 것 마지막으로 디자이너와 스타일을 맞춰본것이 가장 좋은 경험이라고 생각했다! 앞으로 이들과 동등한 위치에서 소통하기 위해서 내 실력또한 빠르게 업그레이드 되야겠다는 생각이 들었다.
댓글남기기