data lab

react native - Expo 공부 2 -구글 플레이 콘솔 개발자 계정 등록하기

LAB 관리자 2024. 7. 7. 19:41
반응형

구글 플레이콘솔 개발자 계정 등록했다. 25000원인가 냈다.

 

개인정보 처리방침은 로폼 '자동문서 작성'에서 작성해서 pdf를 뽑았는데 pdf 안에 글씨가 전혀 인식이 안돼서 클로드한테 물어봤다.

구글드라이브에 올린 다음에 구글 독스로 열라길래 해보니까 그냥 문서화됐다. 나이스

노션에 그대로 붙여넣고 웹 퍼블리시 한다음에 링크 붙여서 냈다. 또 뭐가 필요하니?

 

다시 vs code로 돌아와서, react native 를 expo로 하던게 떠올랐다. expo start 하니까 또 또 또 expo란 명령어가 없단다. 

npm install expo-cli -g

해서 깔고 다시 expo start 했더니 노드 버전이랑 안맞는댄다. 엑스포 일 안하냐?

결국 npx expo start 하니까 됐다. 

 

npx란 무엇인가

npx란 Node Package Execute의 약자로 Node.js 패키지를 더 쉽게 사용할 수 있도록 해주는 도구라고한다. 로컬에 설치되지 않은 패키지를 사용할 수 있게끔 해주고 늘 최신버전의 패키지를 사용할 수 있도록 해서 버전의 충돌을 막아준단다.

 

아래는 내가 요청한대로 클로드가 짜준 ui 초안이다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>식당 대신 전해드립니다</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f0f0f0;
            color: #333;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
            background-color: white;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .header {
            background-color: #4a4a4a;
            color: white;
            padding: 20px;
            text-align: center;
        }
        .post-list {
            padding: 20px;
        }
        .post {
            background-color: #fff;
            border: 1px solid #ddd;
            border-radius: 4px;
            padding: 15px;
            margin-bottom: 15px;
        }
        .post-header {
            display: flex;
            justify-content: space-between;
            margin-bottom: 10px;
            font-size: 0.9em;
            color: #777;
        }
        .post-content {
            margin-bottom: 10px;
        }
        .post-footer {
            font-size: 0.8em;
            color: #999;
            text-align: right;
        }
        .form {
            padding: 20px;
            border-top: 1px solid #eee;
            background-color: #f9f9f9;
        }
        .input {
            width: 100%;
            padding: 10px;
            margin-bottom: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
        }
        .button {
            width: 100%;
            padding: 10px;
            background-color: #4a4a4a;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .button:hover {
            background-color: #333;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>식당 대신 전해드립니다</h1>
        </div>
        <div class="post-list">
            <div class="post">
                <div class="post-header">
                    <span>#3</span>
                </div>
                <div class="post-content">
                    <strong>가게 이름: 해피 카페</strong><br>
                    오늘 날씨가 정말 좋네요. 해피 카페에서 아이스 아메리카노 한잔하면서 힐링하세요!
                </div>
                <div class="post-footer">
                    2024-07-07 14:30
                </div>
            </div>
            <div class="post">
                <div class="post-header">
                    <span>#2</span>
                </div>
                <div class="post-content">
                    <strong>가게 이름: 달콤 베이커리</strong><br>
                    새로 생긴 달콤 베이커리 추천합니다. 크로와상이 정말 맛있어요.
                </div>
                <div class="post-footer">
                    2024-07-07 11:15
                </div>
            </div>
            <div class="post">
                <div class="post-header">
                    <span>#1</span>
                </div>
                <div class="post-content">
                    <strong>가게 이름: 맛있는 식당</strong><br>
                    안녕하세요! 맛있는 식당에서 새로운 메뉴가 나왔대요. 다들 한번 가보세요!
                </div>
                <div class="post-footer">
                    2024-07-07 09:00
                </div>
            </div>
        </div>
        <div class="form">
            <input type="text" class="input" placeholder="가게 이름">
            <textarea class="input" placeholder="제보내용을 적어주세요" rows="4"></textarea>
            <button class="button">글 작성</button>
        </div>
    </div>
</body>
</html>

 

공부용 리액트 네이티브 코드 초안

아래는 아주 간단한 이와 같은 예시 리액트 네이티브 화면이다

import React from 'react'
import {View, Text, StyleSheet, Image, TouchableOpacity} from 'react-native'

export default function AboutPage() {
    const aboutImage = "https://cdn.pixabay.com/photo/2023/03/28/01/10/money-7881948_640.jpg"
    return(
        <View style={styles.container}>
            <Text style={styles.title}>나는 멋난 앱을 만들어삐</Text>

            <View style={styles.textContainer}>
                <Image style={styles.aboutImage} source={{uri:aboutImage}} resizeMode={"cover"}/>
                <Text style={styles.desc01}>마 돈함 주자</Text>
                <Text style={styles.desc02}>100원만 도</Text>
                <TouchableOpacity style={styles.button}>
                    <Text style={styles.buttonText}>종우에게 돈주기</Text>
                </TouchableOpacity>
            </View>
        </View>
    )
}

const styles = StyleSheet.create({
    container: {
        flex:1,
        backgroundColor:"#1F266A",
        alignItems:"center"
    },
    title: {
        fontSize:30,
        fontWeight:"700",
        color:"#fff",
        paddingLeft:30,
        paddingTop:100,
        paddingRight:30
    },
    textContainer: {
        width:300,
        height:500,
        backgroundColor:"#fff",
        marginTop:50,
        borderRadius:30,
        justifyContent:"center",
        alignItems:"center"
    },
    aboutImage: {
        width:150,
        height:150,
        borderRadius:30
    },
    desc01: {
        textAlign:"center",
        fontSize:20,
        fontWeight:"700",
        paddingLeft:22,
        paddingRight:22
    },
    desc02: {
        textAlign:"center",
        fontSize:15,
        fontWeight:"700",
        padding:22
    },
    button: {
        backgroundColor:"orange",
        padding:20,
        borderRadius:15
    },
    buttonText: {
        color:"#fff",
        fontSize:15,
        fontWeight:"700"
    }
})

식당 대신 전해드립니다 코드 초안

import React, { useState } from 'react';
import { 
  View, 
  Text, 
  TextInput, 
  TouchableOpacity, 
  FlatList, 
  StyleSheet, 
  SafeAreaView,
  KeyboardAvoidingView,
  Platform
} from 'react-native';

const Post = ({ item }) => (
  <View style={styles.post}>
    <View style={styles.postHeader}>
      <Text style={styles.postNumber}>#{item.id}</Text>
    </View>
    <View style={styles.postContent}>
      <Text style={styles.storeName}>{item.storeName}</Text>
      <Text>{item.content}</Text>
    </View>
    <View style={styles.postFooter}>
      <Text style={styles.timestamp}>{item.timestamp}</Text>
    </View>
  </View>
);

const App = () => {
  const [posts, setPosts] = useState([
    { id: 3, storeName: '해피 카페', content: '오늘 날씨가 정말 좋네요. 해피 카페에서 아이스 아메리카노 한잔하면서 힐링하세요!', timestamp: '2024-07-07 14:30' },
    { id: 2, storeName: '달콤 베이커리', content: '새로 생긴 달콤 베이커리 추천합니다. 크로와상이 정말 맛있어요.', timestamp: '2024-07-07 11:15' },
    { id: 1, storeName: '맛있는 식당', content: '안녕하세요! 맛있는 식당에서 새로운 메뉴가 나왔대요. 다들 한번 가보세요!', timestamp: '2024-07-07 09:00' },
  ]);

  const [storeName, setStoreName] = useState('');
  const [content, setContent] = useState('');

  const addPost = () => {
    if (storeName && content) {
      const newPost = {
        id: posts.length + 1,
        storeName,
        content,
        timestamp: new Date().toLocaleString(),
      };
      setPosts([newPost, ...posts]);
      setStoreName('');
      setContent('');
    }
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerText}>식당 대신 전해드립니다</Text>
      </View>
      <FlatList
        data={posts}
        renderItem={({ item }) => <Post item={item} />}
        keyExtractor={item => item.id.toString()}
        style={styles.postList}
      />
      <KeyboardAvoidingView
        behavior={Platform.OS === "ios" ? "padding" : "height"}
        style={styles.form}
      >
        <TextInput
          style={styles.input}
          placeholder="가게 이름"
          value={storeName}
          onChangeText={setStoreName}
        />
        <TextInput
          style={[styles.input, styles.contentInput]}
          placeholder="제보내용을 적어주세요"
          value={content}
          onChangeText={setContent}
          multiline
        />
        <TouchableOpacity style={styles.button} onPress={addPost}>
          <Text style={styles.buttonText}>글 작성</Text>
        </TouchableOpacity>
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f0f0f0',
  },
  header: {
    backgroundColor: '#4a4a4a',
    padding: 20,
    alignItems: 'center',
  },
  headerText: {
    color: 'white',
    fontSize: 20,
    fontWeight: 'bold',
  },
  postList: {
    padding: 20,
  },
  post: {
    backgroundColor: 'white',
    borderRadius: 8,
    padding: 15,
    marginBottom: 15,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.2,
    shadowRadius: 1,
    elevation: 2,
  },
  postHeader: {
    flexDirection: 'row',
    justifyContent: 'flex-end',
    marginBottom: 10,
  },
  postNumber: {
    fontSize: 14,
    color: '#777',
  },
  postContent: {
    marginBottom: 10,
  },
  storeName: {
    fontWeight: 'bold',
    marginBottom: 5,
  },
  postFooter: {
    alignItems: 'flex-end',
  },
  timestamp: {
    fontSize: 12,
    color: '#999',
  },
  form: {
    padding: 20,
    backgroundColor: '#f9f9f9',
    borderTopWidth: 1,
    borderTopColor: '#eee',
  },
  input: {
    backgroundColor: 'white',
    borderRadius: 4,
    borderWidth: 1,
    borderColor: '#ddd',
    padding: 10,
    marginBottom: 10,
  },
  contentInput: {
    height: 100,
    textAlignVertical: 'top',
  },
  button: {
    backgroundColor: '#4a4a4a',
    borderRadius: 4,
    padding: 15,
    alignItems: 'center',
  },
  buttonText: {
    color: 'white',
    fontWeight: 'bold',
  },
});

export default App;

 

코드를 설명해보겠다.

react 라이브러리에서 React 랑 {use state} 를 import하고

react-native 에서 View,부터 Platform까지 싹 import한다

 

객체는 세개다. Post, App 컴포넌트 두개, 그리고 addPost 함수이다.

 

Post 컴포넌트

이 Post 컴포넌트는 게시물 카드 컴포넌트로 계속 재사용된다.

1. 화살표 함수

const Post = ({item}) => ( ''');

{item}은 props로 전달 된 객체에서 item 속성을 구조 분해 할당으로 받아온다.

 

1- item은 왜 {}로 쌓여있으며 저걸로 쌓여있으면 의미가 뭐지?
A: {}는 Js에서 객체를 의미한다. 리액트 JSX 문법에서 이를 써서 Js코드를 삽입한다->?

 

 

2- props가 뭐지?
A: properties의 준말로, react 에서 컴포넌트 간에 데이터를 전달하는 방법이다.
1. 부모에서 자식으로 데이터를 전달한다.
2. 읽기 전용이다. 자식 컴포넌트에서 직접 props를 수정할 수 없다
3. 컴포넌트를 재사용 가능하게 만든다
3- 구조 분해 할당이 뭐지?
A: 구조분해 할당이란, 객체에서 객체의 요소를 꺼내서 사용할 때, const fruitBox={banana:'잘익은바나나',}라면 보통 const banana = fruitBox.banana처럼 (객체).(요소이름)을 사용해 '잘익은 바나나'를 꺼내오는데, 구조분해 할당 방법을 사용하면 const {banana} =fruitBox; 하면 바로 fruitBox 속의 '잘익은 바나나'를 꺼내올 수 있다.

아 이거 props랑 화살표함수랑 구조분해 할당은 그냥 별개의 게시물로 다시 공부하자

 

 

{styles.xxx} 구조의 요소로부터 css를 받아오고

{item.xxx} 구조의 요소로부터 표시할 컨텐츠를 받아온다. 

App 컴포넌트

const [posts, setPosts] = useState([...]);

useState 훅을 사용하여 posts 상태와 이를 업데이트하는 setPosts 함수를 생성한다.

 

4- useState 는 무엇이고 훅은 무엇인가?
A: React16.8부터 도입된 기능으로 함수형 컴포넌트에 state와 생명주기 기능을 사용할 수 있게 해주는 함수들이다. 가장 기본적인 훅으로 useState가 있다.
얘도 게시물 따로.

초기값이 아래와 같은 자료구조로 이루어진 값 세개다.

{
      id: 3,
      storeName: "해피 카페",
      content:
        "오늘 날씨가 정말 좋네요. 해피 카페에서 아이스 아메리카노 한잔하면서 힐링하세요!",
      timestamp: "2024-07-07 14:30",
    }
5- 이거 뭐라그러지 자료구조를? map? 기억이 안나네? 키값이랑 밸류값으로 구성된 자료구조였던거같은데.
A: 파이썬이랑 C#에서는 딕셔너리, 자바에서는 맵이라고 부르며, 자바스크립트에서는 객체 리터럴이라고 부르는 자료구조이다. 
특징으로는, 중괄호 안에 정의되며 키와 밸류 쌍으로 구성되며 각 쌍은 :로 구분되고 쌍들은 서로 , 로 구분된다.

 

const[storeName, setStoreName]= useState('');

const[content, setContent]=useState('');

이 두 useState 훅을 사용해 '가게이름'과 '내용' 상태를 관리한다. 초기값은 빈 문자열이다.

 

6- 여기서 useState 훅을 사용한다는 표현이 맞나?
A: 맞다. 정확하다.

addPost 함수

const addPost =()=>{'''};

7- 어라 얘는 왜 () 안에 {items}같은 존재가 없으며, 왜 화살표 다음이 ();가 아니고 {};이지?
A: 매개변수가 없는 경우이며 함수 본문이 길고 복잡하거나 명시적인 return문이 필요할 경우 사용된다
8- 그리고 이김에 ;를 붙일 때랑 안붙일 때를 다시한번 구분해서 공부해보자
A: ; 는, 문장의 끝에 붙이며, 문장이나 선언이 완전히 끝나는 곳에는 세미콜론을 붙이지 않는다.
블록이나 import문, 클래스 선언 뒤에는 안붙여.

 

if(storeName&&content){아래 코드박스};

9- &&연산자를 비롯한 js 연산자 공부해보자. 이거 존재만 하면 그냥 참으로 인정되는건가?
<논리연산자>
&& and - 둘다 참일때 참
|| or - 두 조건 중 하나라도 참이면 참
! not - 조건의 결과를 반대로 변경
const newPost = {
id: posts.length + 1,
storeName,
content,
timestamp: new Date().toLocaleString(),
};

 

10- js는 띄어쓰기가 중요한가?
A: 하든말든 크게 상관없다.

 

11- timestamp 는 제공하는 변수명인가?
A: 아니다. 내가 작성한 함수명이다.

 

12- posts는 언제 내가 정의? 선언? 했지? length도 제공하는 변수명이겠지? 이런거 또없나?
A:  lenth는 Js에서 배열이 기본제공하는 속성이다. 모든 배열은 lenth속성을 가지며, 이는 배열의 요소 개수를 나타낸다.
이와 비슷하게 push() pop() getDate() 등 다양하게 있다.
13- 저렇게 storeName이랑 content는 쓰기만하면 입력값으로 가는거라고 되는건가? 
A : ㅇㅇ 된다.

 

setPosts([newPost, ...posts]);
14- 이게 무슨뜻이여. 나머지는 비워놓는게 초기값이라는게 이해되는데.
스프레드 연산자는 . . .로 표기하며 ES6에 도입된 기능이라고한다. 반복가능한 객체를 펼치는 역할이다. . . .posts라는 것은, posts배열을 모두 늘어놓게 된다.

 

아래는 firebase와 연결하려고 수정한 코드 (검수안함) 일단 위에 코드로 퍼블리시할거임

import React, { useState, useEffect } from 'react';
import { 
  View, 
  Text, 
  TextInput, 
  TouchableOpacity, 
  FlatList, 
  StyleSheet, 
  SafeAreaView,
  KeyboardAvoidingView,
  Platform
} from 'react-native';
import database from '@react-native-firebase/database';

const Post = ({ item }) => (
  <View style={styles.post}>
    <View style={styles.postHeader}>
      <Text style={styles.postNumber}>#{item.id}</Text>
    </View>
    <View style={styles.postContent}>
      <Text style={styles.storeName}>{item.storeName}</Text>
      <Text>{item.content}</Text>
    </View>
    <View style={styles.postFooter}>
      <Text style={styles.timestamp}>{new Date(item.timestamp).toLocaleString()}</Text>
    </View>
  </View>
);

const App = () => {
  const [posts, setPosts] = useState([]);
  const [storeName, setStoreName] = useState('');
  const [content, setContent] = useState('');

  useEffect(() => {
    const onValueChange = database()
      .ref('/posts')
      .orderByChild('timestamp')
      .on('value', snapshot => {
        const data = snapshot.val();
        const postList = data
          ? Object.keys(data).map(key => ({
              id: key,
              ...data[key],
            }))
          : [];
        setPosts(postList.reverse()); // 최신 글이 위로 오도록 역순 정렬
      });

    // Unsubscribe from events when no longer in use
    return () => database().ref('/posts').off('value', onValueChange);
  }, []);

  const addPost = async () => {
    if (storeName && content) {
      try {
        const newPostRef = database().ref('/posts').push();
        await newPostRef.set({
          storeName,
          content,
          timestamp: database.ServerValue.TIMESTAMP,
        });
        setStoreName('');
        setContent('');
      } catch (error) {
        console.error('Error adding post: ', error);
      }
    }
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerText}>식당 대신 전해드립니다</Text>
      </View>
      <FlatList
        data={posts}
        renderItem={({ item }) => <Post item={item} />}
        keyExtractor={item => item.id}
        style={styles.postList}
      />
      <KeyboardAvoidingView
        behavior={Platform.OS === "ios" ? "padding" : undefined}
        keyboardVerticalOffset={Platform.OS === "ios" ? 64 : 0}
        style={styles.form}
      >
        <TextInput
          style={styles.input}
          placeholder="가게 이름"
          value={storeName}
          onChangeText={setStoreName}
        />
        <TextInput
          style={[styles.input, styles.contentInput]}
          placeholder="제보내용을 적어주세요"
          value={content}
          onChangeText={setContent}
          multiline
        />
        <TouchableOpacity style={styles.button} onPress={addPost}>
          <Text style={styles.buttonText}>글 작성</Text>
        </TouchableOpacity>
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  // ... (이전과 동일한 스타일)
});

export default App;

 

반응형