Material UI

공식 문서

https://material-ui.com/

 

Material-UI: A popular React UI framework

React components for faster and easier web development. Build your own design system, or start with Material Design.

material-ui.com

 

설치하기

yarn add @material-ui/core @material-ui/icons

 

사용하기

Detail.js

import Button from '@material-ui/core/Button';
import ButtonGroup from '@material-ui/core/ButtonGroup';
...

const Detail = (props) => {
	...
	return (
		...
		<ButtonGroup>
			<Button
				variant="outlined"
				onClick={() => { /*생략*/ }}
			>삭제하기</Button>
			<Button
				variant="outlined"
				onClick={() => { /*생략*/ }}
			>완료하기</Button>
		</ButtonGroup>
		...
	);
};

export default Detail;

 


페이지 의도적으로 가리기

페이지 가리기가 필요한 이유

  • 현재는 redux에 넣어둔 초깃값 (가짜 데이터) 이 먼저 보인다.
  • Firestore의 데이터만 제대로 보여주기 위해 데이터를 가져오기 전까지는 페이지를 가린다.
  • 수정 또는 추가하기 버튼을 눌렀을 때 API를 여러번 호출하는 현상도 방지할 수 있다.

 

로딩 스피너 만들기

로딩 스피너 컴포넌트 만들기

  • 머터리얼 UI 아이콘 이용
import React from 'react';
import styled from 'styled-components';
import {Eco} from '@material-ui/icons';

const Spinner = (props) => {
	return (
		<Outter>
			<Eco style={{color: '#673ab7', fontSize: '150px'}}/>
		</Outter>
	);
}

const Outter = styled.div`
	position: fixed;
	top: 0;
	left: 0;
	width: 100vw;
	height: 100vh;
	display: flex;
	align-items: center;
	justify-content: center;
	background-color: #ede2ff;
`;

export default Spinner;

 

bucket.js

  • initialState에 is_loaded 변수를 추가해서 firestore에서 데이터를 받아오면 갱신하도록 함
const initialState = {
	is_loaded: false,
	list: [
		{text: '영화관 가기', completed: false},
		{text: '매일 책읽기', completed: false},
		{text: '수영 배우기', completed: false},
	],
};

...
	case 'bucket/LOAD': {
		if (action.bucket.length > 0) {
			return {list: action.bucket, is_loaded: true};
		}
		return state;
	}

 

App.js

  • is_loaded 값에 따라 조건부 렌더링을 한다.
const mapStateToProps = (state) => {
	bucket_list: state.bucket.list,
	is_loaded: state.bucket.is_loaded,
});
...
render() {
	<div className='App'>
		<Container>
			<Title>내 버킷리스트</Title>
			{!this.props.is_loaded ? (
				<Spinner/>
				) : (
					<React.Fragment>
						<Progress />
						<Line />
						<Switch>
							...
						</Switch>
						</React.Fragment>
				)}
			</Container>
		...

redux-thunk란?

  • Firestore에서 데이터를 가져올 때 비동기 통신을 함
  • 리덕스에서 비동기 통신을 할 때 미들웨어가 필요함
  • 일반 액션 생성 함수는 객체를 반환하는데 redux-thunk는 객체 대신 함수를 생성하는 액션 생성함수를 작성할 수 있게 해줌
  • 함수를 생성하면 특정 액션이 발생하기 전에 조건을 주거나 행동을 할 수 있음

미들웨어

  • 리덕스 데이터를 수정할 때 액션 디스패치 → 리듀서에서 처리
  • 미들웨어가 있으면 액션 디스패치 → 미들웨어가 할일 → 리듀서에서 처리

설치하기

yarn add redux-thunk

configStore.js

  • redux-thunk 적용 전 configStore.js
import {createStore, combineReducers} from 'redux';
import bucket from './modules/bucket';
import {createBrowserHistory} from 'history';

export const history = createBrowserHistory();
const rootReducer = combineReducers({bucket});
const store = createStore(rootReducer);

export default store;
  • redux-thunk 적용 후 ⬇
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
import thunk from 'redux-thunk';
import bucket from './modules/bucket';
import {createBrowserHistory} from 'history';

export const history = createBrowserHistory();

const middlewares = [thunk];

const enhancer = applyMiddleware(...middlewares);
const rootReducer = combineReducers({bucket});
const store = createStore(rootReducer, enhancer);

export default store;

 

Load하기

bucket.js 수정

  • Firebase랑 통신하는 함수 생성
const bucket_db = firestore.collection('bucket');

export const loadBucketFB = () => {
	return function (dispatch) {
		bucket_db.get().then((docs) => {
			let bucket_data = [];
			docs.forEach((doc) => {
				if(doc.exists){
					bucket_data = [...bucket_data, {id: doc.id, ...doc.data()}];
				}
			});

			dispatch(loadBucket(bucket_data));
		});
	};
};
  • 리듀서 수정
case 'bucket/LOAD': {
	if (action.bucket.length > 0) {
		return {list: action.bucket};
	}

	return state;
}

App.js 수정

const mapDispatchToProps = (dispatch) => ({
	load: () => {
		dispatch(loadBucketFB());
	},
	create: (new_item) => {
		dispatch(createBucket(new_item));
	}
});

 

Create 하기

bucket.js 수정

  • Firebase와 통신하는 함수 생성
export const addBucketFB = (bucket) => {
	return function (dispatch) {
		let bucket_data = {text: bucket, completed: false};
		bucket_db
			.add(bucket_data)
			.then((docRef) => {
				bucket_data = {...bucket_data, id: docRef.id};
				dispatch(createBucket(bucket_data));
			});
			.catch((err) => {
				window.alert('오류 발생');
			});
	};
};
  • 리듀서 수정
case 'bucket/CREATE': {
	const new_bucket_list = [
		...state.list,
		action.bucket,
	];
	return {list: new_bucket_list};
}

App.js

const mapDispatchToProps = (dispatch) => ({
	load: () => {
		dispatch(loadBucketFB());
	},
	create: (new_item) => {
		dispatch(addBucketFB(new_item));
	}
});

 

Update 하기

bucket.js

export const updateBucketFB = (bucket) => {
	return function (dispatch, getState) {
		const _bucket_data = getState().bucket.list[bucket];
		if (!_bucket_data.id) {
			return;
		}

		let bucket_data = {..._bucket_data, completed: true};
		bucket_db
			.doc(bucket_data.id)
			.update(bucket_data)
			.then((res) => {
				dispatch(updateBucket(bucket));
			})
			.catch((err) => {
			});
	};
};

Detail.js

<button onClick={() => {
		dispatch(updateBucketFB(bucket_index));
		props.history.goBack();
	}}>완료하기</button>

 

Delete 하기

bucket.js

export const deleteBucketFB = (bucket) => {
	return function (dispatch, getState) {
		const _bucket_data = getState().bucket.list[bucket];
		if (!_bucket_data.id) {
			return;
		}
		bucket_db
			.doc(_bucket_data.id)
			.delete()
			.then((res) => {
				dispatch(deleteBucket(bucket));
			})
			.catch((err) => {
			});
	};
};

Detail.js

<button onClick={() => {
		dispatch(deleteBucketFB(bucket_index));
		props.history.goBack();
	}}>삭제하기</button>

1. 1:1 채팅방 번호 생성

추후에 자신이 속한 채팅방 목록을 쉽게 조회하기 위해서, 그리고 1:1뿐만 아니라 단체톡방 기능 갖도록 확장할 것을 고려하여

채팅방마다 고유의 번호를 가지고 있는 것이 편리할 것이라는 판단을 했다.

따라서 친구 등록 시간과 자신의 아이디를 조합하여 채팅방 번호를 만들었다.

(ex. 2021년 9월 5일 오전 1시 33분 4초에 친구 등록 -> 20210905013304아이디 번호를 가지는 채팅방 생성)

 

자바스크립트에서 날짜를 다루는 함수를 찾아보았는데 사용자 임의로 포맷을 만들어서

그 형식에 맞게 날짜를 리턴하는 함수는 따로 없는 것으로 보인다.

자바는 format 함수가 있어서 편리한데 자바스크립트에는 없어서 의외다.

 

그래서 처음에는 이렇게 코드를 작성했다가

 

    //현재 날짜와 시간을 이용해서 채팅방 번호 생성
    function makeChatRoomNum() {
        const today = new Date();   //현재 날짜
        const year = today.getFullYear();
        const month = ('0' + (today.getMonth() + 1)).slice(-2);
        const day = ('0' + today.getDate()).slice(-2);
        const hours = ('0' + today.getHours()).slice(-2);
        const minutes = ('0' + today.getMinutes()).slice(-2);
        const seconds = ('0' + today.getSeconds()).slice(-2);
        
        return year + month + day + hours + minutes + seconds + loginUserId;
    }

 

좀 더 찾아보니 moment라는 라이브러리가 자바의 format과 같은 기능을 제공한다고 하여 적용해보았다.

 

import moment from 'moment';

	//현재 날짜와 시간을 이용해서 채팅방 번호 생성
    function makeChatRoomNum() {
		return moment().format('YYYYMMDDHHmmss') + loginUserId;
    }

 

8줄의 코드를 단 한줄로 줄일 수 있었다...!

이 외에도 날짜 더하기, 빼기 등 유용한 함수를 많이 제공하는 것으로 보인다.

라이브러리를 잘 활용하는 것이 개발 생산성 향상에 매우 도움된다는 것을 다시 한번 체감한다.

 

 

2. 채팅방 멤버 DB 저장

친구 추가 시 자동으로 만들어지는 1:1 채팅방에

현재 로그인한 사용자와 친구로 등록된 사용자를 멤버로 등록한다.

 

    //친구 추가
    const addFriend = () => {
        let popup = window.confirm('친구로 추가하시겠습니까?');
        if (popup) {    //'예'를 선택했을 때
            console.log('친구 추가하기');
            const chatRoomNum = makeChatRoomNum();

            //친구 등록 완료
            firestore.collection('users').doc(loginUserEmail).collection('friends').doc(friendEmail).set({id: friendId, name: friendName, chatRoomNum: chatRoomNum})
                .then(() => {
                    alert('친구로 등록되었습니다.')}
                );

            //채팅방 멤버로 등록
            firestore.collection('chatRooms').doc(chatRoomNum).collection('members').doc(loginUserEmail).set({isMember: true});
            firestore.collection('chatRooms').doc(chatRoomNum).collection('members').doc(friendEmail).set({isMember: true});
        }
    }

 

chatRooms (컬렉션)

ㄴ채팅방 번호 (문서)

  ㄴmembers (컬렉션)

    ㄴ로그인한 사용자 이메일 (문서)

       ㄴisMember: true (필드)

    ㄴ친구로 등록된 사용자 이메일 (문서)

       ㄴisMember: true (필드)

 

친구 등록이 완료되면 DB에 위처럼 등록이 된다.

 

 

3. 전송한 채팅 메세지 DB 저장

드디어 채팅 기능 구현을 시작했다! 가장 쉬운 채팅 전송 기능부터 만들어보았다.

채팅방에서 메세지를 입력하고 전송 버튼을 클릭하면 DB에 저장된다.

 

const content = React.useRef(); //채팅 input 박스

    function sendMessage() {
        const value = content.current.value;

        if (value.length == 0) {
            alert('채팅 내용을 입력해주세요');
        } else {
            const date = getDate(); //채팅 내역 document에 사용
            const time = getTime(); //채팅 내역 메세지 field에 사용
            const messageCode = time + loginId;

            firestore.collection('chatRooms').doc(chatRoomNum).collection('chatMessages')
            .doc(date).collection('userMessage').doc(messageCode).set({content: value, time: time.slice(0, -3)}).then(
                //전송 완료 (DB 등록)
            );

            content.current.value = ''; //채팅 input 박스 비우기
        }
    }

 

input 박스 ref에 React.useRef로 만든 ref를 등록해서

전송 버튼 클릭 시 해당 input 안에 입력된 값을 가져오고

내용이 입력되었으면 DB에 저장, 만약 입력되지 않았으면 alert 메세지를 띄운다.

 

chatRooms (컬렉션)

ㄴ채팅방 번호 (문서)

  ㄴchatMessages (컬렉션)

    ㄴ날짜 (문서)

      ㄴuserMessage (컬렉션)

        ㄴ메세지 번호 (문서)

          ㄴcontent: 메세지 내용 (필드)

          ㄴtime: 전송 시간 (필드)

 

현재 DB 구조는 이러한데 추후에 수정될 수도 있다.

 

채팅 메세지 번호를 생성할 때도 역시 moment 라이브러리를 이용해서 간단히 날짜와 시간을 얻을 수 있었다.

 

    function getDate() {
        return moment().format('YYYYMMDD');
    }

    function getTime() {
        return moment().format('HH:mm:ss');
    }

 

 

4. 채팅 메세지 레이아웃 만들기

본격적으로 채팅 내역 조회 기능을 구현하기 전에, 먼저 메세지들의 레이아웃을 만들어보았다.

 

 

일반 메신저와 큰 차이점 없이 이름과 메세지 내용, 전송 시간을 나타내도록 했다.

재사용을 위해 보낸 메세지, 받은 메세지를 각각 컴포넌트로 만들었다.

 

채팅 내역 조회까지 구현하면 주요 기능은 거의 완성되는 건데

구현하면서 얼마나 많은 시행착오를 겪게 될지ㅎㅎ 기대된다.

1. 친구 추가 시 Firestore DB에 저장

친구 찾기 기능을 이용하여 사용자 아이디를 검색하면 해당 사용자가 검색 결과로 나온다.

친구 추가하기 버튼을 클릭하면 친구 추가 진행 확인 alert가 뜨고, 확인 버튼을 클릭했을 때 Firestore DB에 저장된다.

 

 

현재 DB의 구조는 다음과 같이 되어있다.

users (컬렉션)

ㄴ사용자 이메일 (문서)

  ㄴfriends (컬렉션)

   ㄴ친구 이메일 (문서)

    ㄴ친구 아이디 (필드)

    ㄴ친구 이름 (필드)

로그인한 사용자의 문서 아래에 있는 friends 목록에 추가한 친구 정보가 등록되는 것이다.

 

이름은 추후에 수정 기능을 제공할 예정이라 필드에 넣고 싶지 않았는데

사이드바에 친구 목록을 불러오는 것을 구현할 때 중첩으로 DB를 읽어와야해서 비동기 처리가 필요한데

이것을 해결하지 못해서 나중에 다시 시도해보기로 하고, 현재는 이름 필드도 추가하게 해놨다.

 

 

2. 사이드바 친구 목록 표시

친구 추가, 그리고 나중에 구현될 친구 삭제 기능을 이용하여 친구 목록이 수정되면 친구 목록이 바로 업데이트될 수 있도록

첫 렌더링 후에 실행되는 useEffect 안에서 Firestore의 onSnapshot 메소드를 이용했다.

 

    const loginEmail = useSelector(state => state.user.email); //redux에서 관리하는 로그인한 사용자 이메일
    const [friends, setFriends] = React.useState([]);	//친구 목록을 상태로 관리함
    const usersFB = firestore.collection('users');	//Firestore에 있는 user 컬렉션

	useEffect(() => {
        usersFB.doc(loginEmail).collection('friends').onSnapshot((docs) => {
            let listFromFB = [];
            docs.forEach((doc) => {
                listFromFB.push({id: doc.data().id, name: doc.data().name, chatRoomNum: doc.data().chatRoomNum});
            });
            setFriends(listFromFB);
    })}, []);

 

DB에서 읽어온 친구 목록을 listFromFB라는 배열에 담고 이것을 상태로 넘겨주면 리렌더링 되면서 사이드바에 친구 목록이 반영된다.

 

 

친구 등록 후 사이드바에 해당 사용자가 추가된 모습이다.

 

 

3. 친구 이름 클릭 시 1:1 채팅방 팝업 띄우기

사이드바에 있는 친구 목록에서 각 사용자를 클릭하면 해당 사용자와 1:1 채팅을 할 수 있도록 채팅방 팝업을 띄운다.

리액트 라우팅에서 URL 파라미터를 이용하여 채팅 상대에 대한 정보를 전달한다.

 

 

전체적인 레이아웃 영역만 잡은 상태이고 조만간 채팅 메세지 레이아웃을 만들 예정이다.

신입사원 기본역량교육 두번째 날!

이 날은 오전 4시간, 오후 4시간동안 교육이 진행돼서 교육이 끝나고 나서 지친 상태였다.

그래도 얻어가는게 있으니까 교육 시간에 열심히 참여했다!

 

오전에는 문제해결역량 교육이 있었다.

 


문제 정의의 중요성

문제를 해결하기 위해서는 먼저 문제를 정의하는 과정이 필요하다.

그런데 문제를 어떻게 정의하느냐에 따라서 복잡한 해결책이 나올 수도, 간단한 해결책이 나올 수도 있는 것이다.

 

강사님이 언급하신 예시는 "우주에서 사용할 수 있는 필기구"에 대한 문제에 대해

미국은 "무중력 상태에서도 볼펜을 사용할 수 있는 방법",

소련은 "무중력 상태에서 우주 실험을 기록할 수 있는 가장 효과적인 방법"으로 문제 정의를 한 사례가 있었다.

 

 

이미지 자동 분류를 위한 접근 방식 토의 실습

말 그대로 이미지를 자동 분류하기 위해서는 어떤 기술과 방법이 필요한지 토의해보는 시간이었는데

사실 전공 또는 관심 분야가 IT쪽인 사람들이 모였다보니, 다들 딥러닝 또는 머신러닝을 떠올렸다.

우리 팀은 각자의 의견을 모아 "여러 샘플 데이터를 이용하여 모델 학습을 시킨 후 차이가 나는 특징들을 추출함"으로써 이미지 분류를 할 수 있다고 아이디어를 적었다.

 

 

5why 문제 해결 기법

사고역량강화를 위한 기법 중 하나로 5why 문제 해결 기법을 배웠다.

적용 사례 중 하나는 제퍼슨기념관 외벽 손상 사례에 대한 것이었는데

Why1 : 외벽 부식이 강한 이유는? -> 비누 청소를 자주 하기 때문

Why2 : 비누 청소를 자주 하는 이유는? -> 비둘기 배설물이 많기 때문

Why3 : 비둘기 배설물이 많은 이유는? -> 거미가 많기 때문

Why4 : 거미가 많은 이유는? -> 불나방이 많기 때문

Why5 : 불나방이 많은 이유는? -> 직원들이 일찍 퇴근하기 위해 전등을 일찍 키기 때문

결국 5번의 Why로 늦게 전등을 키는 해결 방법을 얻을 수 있었다고 한다.

 

나는 왜 이 일을 하고 싶은가? 브레인스토밍

사고 역량에는 창의적 사고, 논리적 사고, 비판적 사고가 있는데 창의적 사고 기법 중 하나가 브레인스토밍이다.

브레인스토밍은 자유분방하게 아이디어를 제시하는 것인데

나는 왜 이 일(희망 진로)을 하고 싶은가? 라는 주제로 브레인스토밍 실습을 했다.

 

줌의 주석 기능을 이용하여 강의를 듣는 사람들이 각자 자신의 생각을 적어보았는데

 

금전적인 수단, 내가 가장 잘 할 수 있는 일, 틀에 박히지 않은 일, 보다 편리한 세상을 만들 수 있어서 등의 의견이 있었다.

 

나는 FE개발자가 목표라, "작성한 코드에 따라 시각적 결과물이 나오는게 흥미로워서" 라는 의견을 작성했다.

나의 희망 진로를 꿈꾸는 이유를 떠올리며 공부를 하다보면 꾸준히, 포기하지 않고 열심히 달릴 수 있을 것 같다.

 

 

해적 금화 나누기 문제 실습

논리적 사고 실습 시간으로 "해적 금화 나누기" 문제를 팀별로 풀어보았다. (해당 문제는 인터넷에 검색하면 쉽게 찾을 수 있으므로 설명은 생략한다.)

각 해적에게 분배할 금화의 액수를 정해야 하는데 그 값을 정하기 위해 팀원들과 다양한 의견을 주고받았다.

여러 의견을 듣고 각 의견을 종합하여 가장 합리적이라고 생각되는 답을 찾아내서 실습 자료에 적었다.

나는 평소에도 어떤 주제에 대해 내 생각을 전달하며 생각의 근거를 대는 논리적인 대화를 좋아하기 때문에

이번 실습이 굉장히 재밌었다. 다른 사람들과 각자의 의견을 펼칠 수 있는 장이 열린 느낌ㅋㅋㅋ

 

 

문제 해결 실습

강의에서 배운 내용들을 종합하여 해결하고자 하는 문제 선정, 문제 원인 분석, 해결안 도출 시간을 팀별로 가졌다.

 

우리 조가 선정한 문제는 유튜브에서 한국어 댓글을 찾기 어렵다는 점이었다.

유튜브를 이용하는 사람들은 한번쯤은 공감했을 것이다.

외국인 시청자가 많은 영상일수록 한국 영상임에도 불구하고 한국인이 작성한 댓글을 찾기가 굉장히 어렵다.

 

이것의 원인은 한국어 댓글이 댓글 순서 알고리즘 상 낮은 우선순위를 차지하고 있기 때문이라고 분석했다.

따라서 각 언어별 댓글 분류 기능을 이용하여 사용자가 접속한 나라의 언어, 또는 사용자가 선택한 언어의 댓글을 우선순위로 보여주는 기능을 제공하면 해결할 수 있을 것이라 작성했다.

 

 

이번 강의에서 배운 내용들을 바탕으로 실생활 또는 조직에서 발생하는 문제들을 정확히 파악하고, 적합한 해결책을 도출할 수 있도록 문제해결능력을 키워야겠다.

여름방학 기간에 집중적으로 마음이 앱 개발을 진행했다!

앱 개발 기획 시 생각했던 주요 기능은 AI 상담 채팅, 진단테스트, 일기장, 게시판이 있었고

7월에 해당 기능들을 모두 구현했다.

 

또한 한이음 중간점검 보고서 제출 마감일이 7월 19일까지라서 중간보고서와 제작설계서를 작성했었다.

 


 

게시판

6월에 게시판 레이아웃 틀을 잡고 게시글 등록 기능을 구현했었고

7월에는 다음과 같은 기능들을 구현하여 게시판 개발을 마무리했다.

  • 게시글 수정/삭제 기능
  • 댓글 등록/삭제 기능
  • 공감 기능
  • 댓글 수, 공감 수 출력

 

현재 게시판은 자유게시판과 익명게시판으로 나뉜다.

추후에 진단테스트 결과에 따라 마음 온도가 낮게 나온 사용자에 대해 게시판 기능 제한을 둘 예정인데

게시판의 분류가 애매하다는 생각이 들어서 어떻게 변경할지 고민할 필요가 있었다.

 

게시글 목록과 글 내용 페이지

 

진단테스트

진단테스트 문항은 JSON 파일에 저장해놓고, parsing하여 화면에 출력하도록 했다.

게시판과 마찬가지로 RecyclerView를 이용해서 구현하였는데

 

처음 구현하고 나서 테스트를 해보니 사용자가 선택한 응답의 체크 표시가 사라지고,

응답하지 않은 문항에 체크가 되어있는 이상한 현상이 있었다.

 

다행히도 이러한 현상에 대해 다룬 포스팅을 찾을 수 있었다.

https://kawaiineko.tistory.com/19

 

[android] Listview Checkbox 스크롤 후 체크 이상현상 해결법

안드로이드 개발은 쉬우면서도 어렵다. 이게 무슨 개소리냐면 머리로 구상한대로만 진행된다면 정말 그 어떤 코딩보다도 쉬운데 구상한대로 진행하다보면 꼭 생각지도 못한 문제가 발생한다.

kawaiineko.tistory.com

 

RecyclerView의 재사용 처리로 인해 이러한 현상이 발생하는 것이라고 한다.

체크한 응답이 코드상에서 저장은 되는데, 화면에 출력될 때 이상하게 나오는거라

화면에 선택했던 응답에 체크 표시를 하도록 코드를 추가함으로써 해결할 수 있었다.

 

테스트 진행 화면과 결과 화면

 

AI 상담 채팅

앱의 핵심 기능이라고 할 수 있는 AI 상담 채팅 기능!

학교폭력 관련 앱이므로 앱 이용자와 전문가가 직접 상담을 할 수 있다면 좋겠지만 지금 우리가 구현하긴 힘들다고 생각되어

이것의 대체 방안으로 AI 학교폭력 상담 채팅 기능을 제공하기로 한 것이다.

 

Google Dialogflow 챗봇 API를 이용하여 기능 구현을 했는데

구글 서비스 계정에서 문제가 여러 차례 발생하여 Android Studio와 연동하기까지 많은 어려움이 있었다.

지금은 다행히 해당 문제를 해결하여 연동이 완료되었고

앱에서 Dialogflow에 채팅을 전송하고, 응답을 받는 코드 구현 방법을 유튜브에서 찾을 수 있어서 쉽게 구현했다.

 

다만, 원래 Dialogflow는 Custom payload를 이용하여 카드 형식으로 Dialogflow에 보낼 채팅을 선택할 수 있었는데

앱으로 구현할 땐 그러한 응답 형식을 구현할 수 없었다.

사용자가 메세지를 1개 보내면 그에 대한 챗봇 응답 1개만 텍스트 형식으로 받을 수 있는 로직이었기 때문이다.

따라서 웰컴 메세지 (채팅을 시작할 때 첫 메세지) 를 추가하여 사용자에게 예상 질문 목록을 보여주는 것으로 보완했다.

 

채팅 내역은 Firebase DB에 저장하고 불러오도록 코드를 작성하였으며

채팅 내역 전체 삭제 기능을 추가하여 편하게 웰컴 메세지를 다시 확인할 수 있도록 하였다.

 

채팅 화면, 웰컴 메세지, 채팅 내역 전체 삭제 기능

 

기타 개발

  • 사이드바 추가 (Navigation Drawer 이용)
  • 로그아웃 기능 추가 (Firebase Authentication 이용)
  • 앱 디자인 개선

 

8월에는 진단테스트 결과에 따른 기능 제한과 마음 채우기 (영상 시청) 기능 구현을 주 목표로 할 것이고

그 외 앱의 디자인 개선과 각 기능의 완성도를 높이기 위해 다양한 방법을 생각해보아야겠다.

 


 

7월 개발 일지 목록

  • 7/1 (게시글 수정/삭제, 댓글 등록/조회 기능 구현)
 

2021.07.01 개발 일지 (게시글 수정/삭제, 댓글 등록/조회)

1. 게시글 내용 페이지 - 게시글 목록 불러오는 것까지 구현했었는데 이제 각 게시물 카드를 터치했을 때 게시글 세부 내용을 확인하는 페이지로 이동하도록 추가함 2. 게시글 수정 및 삭제 - 작

askges20.tistory.com

 

  • 7/2 (댓글 목록 레이아웃 수정, 댓글 삭제 기능 추가)
 

2021.07.02 개발 일지 (데이터 중복 출력 오류 해결, 댓글 삭제 기능)

1. 댓글 목록 ListView height 조절 - ScrollView 안에 모든 요소를 넣고 그 중에서 ListView가 있는 구조인데 ListView만 스크롤이 되고 전체 ScrollView는 스크롤이 되지 않는 상태였음 - ListView의 각 item들..

askges20.tistory.com

 

  • 7/4 (Android Studio - Firebase 다시 연결)
 

2021.07.04 개발 일지 (Android Studio - Firebase DB 다시 연결)

새로운 Firebase 프로젝트 DB와 안드로이드 스튜디오 연결하기 오늘은 회의가 있는 날이었다. 일기장, 게시판 주요 기능 구현은 거의 다 마친 상태에서 앞으로 구현해야할 기능들에 대해 얘기를 나

askges20.tistory.com

 

  • 7/10 (상담 채팅 레이아웃 구성, 로그인 유지 구현, 마이페이지 사용자 정보 출력)
 

2021.07.10 개발 일지 (1:1 상담 레이아웃 구성, 로그인 유지, 마이페이지 사용자 정보 출력)

1. 1:1 상담 채팅(dialogflow) - 앱 내의 레이아웃 구현 - https://www.youtube.com/watch?v=zVxDBBCdpfY 이 영상에 나온 방법대로 구현 - 메세지 전송 시 화면에 해당 메세지가 출력되는 상태 - 현재 dialo..

askges20.tistory.com

 

  • 7/11 ~ 7/12 (게시판 사용자 아이디 출력, 앱 테마 및 디자인 개선, 진단테스트 기능 구현)
 

2021.7.11 - 7.12 개발 일지 (게시판에 사용자 아이디 반영, 앱 테마 및 디자인 개선, 진단테스트 기능

새벽 시간에 완전 집중해서 개발했다!! 덕분에 많은 진전이 있었다. 슬슬 앱의 디자인적인 요소를 신경써야할거 같아서 폰트나 테마를 수정해봤고 주요 기능인 진단테스트 구현을 하루만에 거

askges20.tistory.com

 

  • 7/13 (Android Studio - Dialogflow 연결 완료, 채팅 내역 Firebase DB에 저장, 불러오기)
 

2021.07.13 개발 일지 (Android Studio - Dialogflow 연결 완료)

1. 게시글 작성 페이지 레이아웃 수정 2. Dialogflow - Android Studio 연결 완료 3. 1:1 상담 채팅 내역 Firebase DB 저장 4. 1:1 상담 채팅 내역 Firebase DB에서 읽어오고 화면에 띄우기 어제는 정말 감격스러..

askges20.tistory.com

 

  • 7/14 ~ 7/15 (Firebase Authentication 로그아웃 기능 구현)
 

2021.07.14 - 7.15 개발 일지 (로그아웃 기능 구현)

1. 로그아웃 기능 추가 - 마이페이지에 로그아웃 버튼 추가 - 파이어베이스 Authentication에서 로그아웃 필요 FirebaseAuth.getInstance().signOut(); - 로그아웃을 진행하면 기존에 실행되던 액티비티를 모두

askges20.tistory.com

 

  • 7/19 (게시판 공감 기능 추가, 사이드바 추가, 채팅 날짜 출력)
 

2021.07.19 개발 일지 (게시판 공감 기능 추가, 사이드바 구현, 채팅 날짜 표시)

일주일 전에 정보처리기사 실기 시험 보고나서 한동안 아무 의욕 없이 지내다가 오늘에서야 다시 의욕이 되돌아온듯 하다. 그동안 많이 쉰만큼 오늘은 12시간동안 빡세게 개발했지!⭐🤸‍♀️

askges20.tistory.com

 

  • 7/24 (게시판 댓글 개수 출력, 익명게시판 댓글 영역 삭제)
 

2021.07.24 개발 일지 (댓글 개수 표시, 익명게시판 댓글 영역 제거)

1. 자유게시판 댓글 개수 표시 - 공감 개수 표시는 지난번에 구현 완료했었고 이번에 댓글 개수 표시까지 완료함 2. 익명게시판 목록에서 댓글 개수 표시X, 글 내용 페이지에서 댓글 영역 제거 3.

askges20.tistory.com

 

  • 7/29 (진단테스트 결과 내용 추가, 채팅 전체 삭제 기능 구현, 채팅 웰컴 메세지 출력)
 

2021.07.29 개발 일지 (진단테스트 결과 상세 내용, 채팅 전체 삭제, 채팅 웰컴 메세지 추가)

1. 진단테스트 결과 출력 - 테스트 점수에 따라 피해, 가해 정도를 [아주 약함 / 약함 / 보통 / 심함 / 아주 심함]으로 분류하여 상세 진단 내용 출력 - 프로그레스바로 최대 점수 중 몇 점을 차지했

askges20.tistory.com

 

1. 친구 검색 바로 업데이트하기

부모 컴포넌트(FindFriends.js)에서 검색할 아이디를 입력하면 props로 넘겨서

자식 컴포넌트(FriendProfile.js)가 바뀌어서 검색 결과를 보여주도록 의도했다.

 

그런데 처음 검색할 때는 제대로 출력이 되지만 두번째로 검색을 할 때는 FriendProfile.js가 바뀌지 않는 것이다.

FindFriends.js에서 검색한 아이디를 상태로 관리하기 때문에

예상대로라면 FriendProfile.js의 props 값이 바뀌므로 출력되는 내용이 바뀌어야하는데 그렇지 않았다.

 

라이프 사이클 함수를 이용하면 해결될까 싶어서 FriendProfile.js를 함수형 컴포넌트에서 클래스형 컴포넌트로 바꿔보기도 했으나 아무 변화가 없었다.

 

관련 자료를 찾아보다가 뜻밖의 해결책을 발견했다.

 

https://www.py4u.net/discuss/976613

 

React: why child component doesn't update when prop changes

Answer #11: I was encountering the same problem. I had a Tooltip component that was receiving showTooltip prop, that I was updating on Parent component based on an if condition, it was getting updated in Parent component but Tooltip component was not rende

www.py4u.net

 

 

나에게 도움이 된 답변은 Answer #6으로, key값을 추가하면 제대로 작동한다는 것이다.

그리고 이 방법대로 했더니, 정말 잘 작동되는 것이다!

리액트에서의 key값의 의미를 좀 더 찾아봐야겠다.

 

그리고 중첩 삼항 연산자를 사용할 부분이 생겨서 관련 자료를 찾아보았다.

 

https://nm-it-diary.tistory.com/38

 

[JAVA] 삼항 연산자 사용법 - 여러개 중첩으로 사용하기

if문 대신 삼항 연산자로 간단한 조건문을 구현할 수 있습니다. if문과 비교하여 삼항 연산자에 대해 알아보겠습니다. 1. if문 예제 int num = 5; String result = ""; if( num == 5 ) { result = "num은 5"; } el..

nm-it-diary.tistory.com

 

삼항 연산자는 JAVA와 JS의 문법이 같은 듯 하다.

 

 

코드 수정 이후 검색을 한번 하고 바로 이어서 다른 아이디를 검색하는 것이 가능해졌다.

그리고 검색 결과가 없을 때 메세지를 출력하도록 수정했다.

 

2. 로그아웃 시 웰컴 화면으로 이동하기

redux를 이용하여 로그인 사용자 상태를 관리하는 것으로 변경되어서

로그아웃 시 redux로 관리하는 상태를 초기화해야 웰컴 화면으로 이동하게 되었다.

 

redux 모듈에 초기값으로 상태를 변경하는 액션 생성 함수 및 액션을 추가하고

useDispatch를 이용해서 해당 액션 생성 함수를 실행시켰다.

 

redux 모듈인 user.js에서 관련 부분만 나타내면 이렇다.

//Actions
const RESET_USER = 'user/RESET_USER';

const initialState = {
    email: '',
    id: '',
    name: '',
    is_loaded: false,
}

//Action Creators
export const resetUser = () => {
    return {type: RESET_USER};
}

//Reducer
export default function reducer(state = initialState, action = {}){
    switch(action.type){
        //do reducer stuff
        case 'user/RESET_USER': {
            return initialState;
        }
        default:
            return state;
    }
}

 

useDispatch를 이용해서 resetUser 액션 생성 함수를 실행하는 부분이다.

import { useDispatch } from 'react-redux';

const Main = (props) => {
    const dispatch = useDispatch();
    
    const logout = () => {
        let popup = window.confirm('로그아웃 하시겠습니까?');
        if (popup) {    //'예'를 선택했을 때
            userSignOut();  //Firebase Authentication 로그아웃
            dispatch(resetUser());  //redux 유저 정보 초기화
            history.push('/'); //웰컴 화면으로 이동
        }
    }
    ...
}

 

1. 화면 레이아웃 잡기

웹페이지의 레이아웃을 잡기 위해 Figma로 간단히 프로토타입을 그려보았다.

 

전체적인 색감이 정해진건 아닌데 프로토타입은 핑크-보라 계열로 그려봤다.

 

레이아웃은 크게 상단바, 사이드바, 본문 영역으로 나뉜다.

 

  • 상단바 왼쪽에는 사이트 이름(미정), 오른쪽에는 탭 메뉴가 있다.
  • 사이드바에는 사용자 프로필과 친구 목록이 나오도록 하여 친구 이름을 클릭했을 때 해당 사용자와 채팅하는 화면으로 넘어가는 것을 생각했다.
  • 상단바 메뉴를 클릭했을 때 클릭한 메뉴에 따라 본문 영역의 내용이 바뀌도록 한다.

 

프로토타입을 바탕으로 친구 검색 화면, 채팅 화면 레이아웃을 구현했다.

 

친구 검색 화면
채팅 화면

우선은 기능 구현을 위해 각 영역만 잡고 디테일한 CSS는 나중에 수정할 것이다.

 

 

2. 친구 검색하기

채팅을 할 친구를 추가하면 사이드바 친구 목록에 추가될 것이고, 그 친구에게 채팅을 보낼 수 있다.

따라서 아이디를 이용해서 다른 사용자를 검색하는 기능을 구현했다.

사용자 정보는 Firestore에 저장되어 있으므로 forEach문으로 조회한다.

 

    const friendId = props.friendId;
    const [isLoaded, setLoaded] = React.useState(false);
    const [friendName, setFriendName] = React.useState('');

    //firebase firestore에서 해당 유저 검색
    const findUser = () => {
        firestore.collection('users').get().then((docs) => {
            docs.forEach((doc) => {
                if (doc.data().id == friendId) {
                    setFriendName(doc.data().name);
                    setLoaded(true);
                }
            });
            setLoaded(true);
        })
    }

 

상위 컴포넌트에서 props로 받아온 아이디를 검색하는 코드이다.

 

 

해당 아이디와 일치하는 사용자가 있으면 화면에 나타난다.

현재는 테스트 편의성을 위해 자신의 아이디도 검색 가능하도록 했다.

 

 

3. 사용자 정보 Redux로 관리하기

로그인 이후부터는 로그인한 사용자 정보가 컴포넌트 곳곳에서 사용될 것인데,

이를 위해서는 전역 상태 관리가 필요하다고 느꼈다.

이것을 위해 Redux(리덕스)를 이용하여 로그인한 사용자 정보를 관리하도록 구조를 변경했다.

 

configStore.js

import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import user from './modules/user';
import { createBrowserHistory } from 'history';

export const history = createBrowserHistory();

const middlewares = [thunk];

const enhancer = applyMiddleware(...middlewares);
const rootReducer = combineReducers({user});
const store = createStore(rootReducer, enhancer);

export default store;

 

user.js

import {firestore} from '../../services/firebase';

const user_db = firestore.collection('users');

//Actions
const GET_USER = 'user/GET_USER';
const IS_LOADED = 'user/IS_LOADED';

const initialState = {
    email: '',
    id: '',
    name: '',
    is_loaded: false,
}

//Action Creators
export const loadUser = (email, id, name) => {
    return {type: GET_USER, data: {email:email, id:id, name:name}};
}

export const isLoaded = (loaded) => {
    return {type: IS_LOADED, loaded};
}

//DB에서 사용자 정보 읽어오는 함수
export const getUserFB = (email) => {
    console.log('액션 생성 : DB에서 유저 정보 읽어오기');
    return function (dispatch){
        user_db.doc(email).get().then((info) => {
            const id = info.get('id');
            const name = info.get('name');
            dispatch(loadUser(email, id, name));    //액션 발생시키기
            dispatch(isLoaded(true));
        })
    }
}

//Reducer
export default function reducer(state = initialState, action = {}){
    switch(action.type){
        //do reducer stuff
        case 'user/GET_USER': {
            return {email: action.data.email, id: action.data.id,
                name: action.data.name};
        }
        case 'user/LOADED': {
            return {...state, is_loaded: action.loaded};
        }
        default:
            return state;
    }
}

 

getUserFB가 Firestore에서 사용자 정보를 가져오는 부분이고

loadUser이 사용자 정보 상태를 업데이트 하는 액션을 생성하는 함수이다.

Reducer에서 상태 변경을 반영한다.

 

index.js에 Provider로 store를 주입한다.

ReactDOM.render(
  <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
  </Provider>,
  document.getElementById('root')
);

 

App.js에서 mapStateToProps와 mapDispatchToProps를 작성해서 상태를 만들고 Redux의 함수를 연결했다.

//스토어가 가진 상태값을 props로 받아오기 위한 함수
const mapStateToProps = (state) => {
  return {
    user_email: state.user.email,
    user_id: state.user.id,
    user_name: state.user.name,
    is_loaded: state.user.is_loaded,
  };
}

//상태 값을 변화시키기 위한 액션 생성 함수를 props로 받아오기 위한 함수
const mapDispatchToProps = (dispatch) => {
  return {
    load: (email) => {
      dispatch(getUserFB(email));
    },
  }
}

 

Profile.js에서 사이드바에 이름과 아이디가 출력되도록 useSelector을 이용했다.

import { useSelector, useDispatch } from 'react-redux';

function Profile(props) {
    const id = useSelector(state => state.user.id);
    const name = useSelector(state => state.user.name);

    return(
        <ProfileConatiner>
            <ProfileImg/>
            <h3>{name}</h3>
            <p>@{id}</p>
        </ProfileConatiner>
    )
};

 

 

직접 코드를 작성해보니 Redux에 대한 개념이 굉장히 헷갈렸지만

다행히도 몇시간 고민하다가 제대로 이해할 수 있었다.

+ Recent posts