SpreadSheet 란 무엇일까?

행과 열로 구분된 표(시트) 형식으로 데이터를 저장하고 계산하고 분석할 수 있는 컴퓨터 어플리케이션이다. 

 

최초의 스프레드시트

VisiCalc

최초의 스프레드시트는 1979년에 출시된 VisiCalc라는 프로그램이다. (2025년도 기준으로 46년 전이다.)

Apple II 컴퓨터에서 최초로 출시되었다.

Accounting paper

Dan Bricklin이 하버드에서 MBA를 할 때, 교수님이 칠판에 Accounting paper와 비슷하게 테이블을 만들고 수식 계산하는것을 보고 고안했다.

그리고 Bob Frankston과 함께 VisiCalc라는 소프트웨어를 만들었다.

컴퓨터 덕후들의 취미로만 사용되던 PC(Personal Computer)를 업무용으로 사용할 수 있게 해준 기념비적인 발명품이라고 볼 수 있다.

누군가 c로 만들어놓은 코드가 있어서 가져와봤다.
https://github.com/geoffmoss0/VisiCalc?tab=readme-ov-file

 

Lotus 1-2-3의 등장

Apple II 기반으로 개발됐던 VisiCalc는 해가 지나며, IBM PC 등 다른 컴퓨터에서도 구동되도록 포팅하였다.

IBM PC에서도 구동은 되었지만, 최적화 되지 않아 IBM PC의 성능를 제대로 쓰지 못하였다. 

그럼에도 SuperCalc나 MS excel의 전신인 Multiplan 같은 경쟁 소프트웨어에 우위를 점하고 있었다.

 

1981년 VisiCalc를 만든 VisiCorp는 VisiPlot/VisiTrend 프로그램을 인수하였고, 개발자였던 Mitch Kapor도 VisiCorp의 직원이 되었다.

Mitch Kapor는 VisiCorp를 나와 Lotus Development Corporation를 만들고 1983년 Lotus 1-2-3을 출시하였다.

lotus 1-2-3

Lotus 1-2-3은 IBM PC의 MS-DOS에 최적화된 프로그램이었고, 스프레드시트 기능, 차트, 데이터베이스, 매크로 기능들을 추가해 점유율을 빠르게 확대했다.

그렇게 점유율이 점점 떨어지던 VisiCalc는 1985년 Lotus에 인수되었다.

 

Excel

MS는 1982년 Multiplan이라는 스프레트시트 소프트웨어를 만들었다.

하지만 자신들이 만든 OS인 MS-DOS에서는 VisiCalc/Lotus 1-2-3에 밀려 주류가 되지 못하였다.

오히려 매킨토시에서 더 많이 쓰이고 있었다.

MS는 이것을 발판 삼아, 1985년 Multiplan에서 Excel로 이름을 바꾸고, GUI기반의 스프레드시트를 매킨토시에서만 출시하였다.

excel

마우스 기반의 인터페이스로 사람들이 더 쉽게 사용할 수 있게 되었다.  

이후 MS는 Windows OS를 출시하면서 Windows용 Excel을 함께 출시하였고, 시장점유율을 점점 높여갔다.

 

MS-DOS시장에서 강세였던 Lotus는 GUI 환경으로의 변화에 대응하지 못하고, VisiCalc의 수순을 밟게 되었다.

 

현재 서비스 되고 있는 SpreadSheet

  • 가장 유명한 MS Excel이 있다.
  • Google SpreadSheet 도 공유문서로 많이 사용된다.
  • 사용하는 사람은 못봤지만, Apple Numbers도 존재한다.
  • 중국에서 만드는 WPS Sheet도 존재한다.
  • 오픈소스로는 LibreOffice Calc가 존재한다.
  • 국내에서는 이정도 있을 것 같다.
    • 한컴 한셀
    • 폴라리스오피스 Sheet
    • 사이냅소프트의 Cell(네이버 오피스 였던)
    • 티맥스오피스 Cell
  • 프로그램은 아니지만, JS에서 사용할 수 있는 스프레드시트 라이브러리인 MESCIUS의 SpreadJS도 있다.

 

출처

https://en.wikipedia.org/wiki/VisiCalc

https://www.cctoday.co.kr/news/articleView.html?idxno=861595

https://medium.com/@slow_scale/%EC%97%91%EC%85%80%EC%9D%98-%EC%A7%84%ED%99%94-%EA%B7%B8%EB%A6%AC%EA%B3%A0-bati-63eacde230f9

https://www.ddanzi.com/ddanziNews/3074580

정의란?

C++에서 정의는 객체를 "실체"로 만들겠다는 뜻이다.

실체로 만들겠다 라는 것은 메모리에 할당하는 것을 의미한다. 메모리 상에 유효한 비트로 만들겠다는 것이다.

#include <iostream>

class CTest;

int g_Val;
int g_Arr[3];
CTest g_T;

int main()
{
	
    int v;
    int arr[3];
}

int Add(int a, int b)
{
	
    return a + b;
}

main 위에 정의한 전역변수는 데이터 영역에 , main 안에 있는 지역변수 스택 영역에 , Add함수 자체도 코드영역에 들어가기 때문에 모두 실체화 한거라고 볼 수 있고, 정의했다고 말할 수 있다.

이런 변수나 함수는 프로세스가 사용하는 가상 메모리이다.

class CTest
{
public:
	int m_Value;
    int MFunc();
}

이런 클래스 정의 자체는 프로세스와 상관 없이 컴파일러가 사용하는 메모리이다.

(함수도 같은거 아닌가? 둘 다 코드영역 아닌가? 함수는 포인터 형태로 프로세스가 사용하기 때문에 성격이 다른건가? 클래스 코드 자체는 컴파일 할 때만 참고하기 때문에 성격이 다르다고 하는건가?)

 

선언이란?

위에서 봤던 클래스, 전역변수, 지역변수, 함수 모두 선언한 것이다.

정의라고 해놓고 또 선언이란다. 무슨말이냐?

이유는 정의는 선언을 포함하기 때문이다. 정의를 한 순간 해당 개체는 알려지므로(선언되므로) 따로 선언해 줄 필요가 없다.

그럼 정의만 있어도 되는 거 아니냐?

선언이 꼭 필요한 경우가 있음.

// A.cpp
int g_Val;
int Func()
{
	return 1;
}
// Main.cpp
void main()
{
	g_Val = Func();
}

이렇게 사용하면 컴파일 에러가 발생한다.

g_Val과 Func()는 정의와 동시에 선언 되었지만, A.cpp 에 한정된다.

main도 g_Val과 Func()에 대해 알 수 있게 해주어야 한다.

// Main.cpp

extern int g_Val;
int Func();

void main()
{
	g_Val = Func();
}

따로 헤더파일을 만들어 include를 해주지 않아도, 각 cpp 파일이 obj파일로 만들어지고 심볼릭 테이블에서 정의를 찾아 매핑해주기 때문에 이 코드로도 컴파일/실행이 가능하다.

#include "A.cpp"

void main()
{
    g_Val = Func();
}

이렇게 하면 되는거 아니야?

아니다. 링킹 오류가 난다.

include는 전처리과정에서 복붙된다.

그래서 최종 obj 파일을 링킹할 때 정의가 2개씩 존재하기 때문에 링커가 선택할 수 없다.

extern & static

extern 외부에서 사용할 수 있음을 알려주고, static은 사용 범위가 한정된다는 의미를 알려준다.

extern은 선언할 때만 사용하는 것이 아니라, 정의할 때도 사용된다.

extern int a; // 선언만 됨.
extern int b = 3; // 선언과 정의가 동시에 됨.

int c; // 선언과 정의가 동시에 됨.
int d = 3; // 선언과 정의가 동시에 됨.

일반적으로 `extern int b = 3;`은 `int b = 3;`과 의미가 똑같기 때문에 생략한다.

// A.cpp
extern int g_Val;

// Main.cpp
#include <iostream>
extern int g_Val;

void main()
{
	printf("%d", g_Val);
}

그래서 이렇게 선언만 있는 경우, 링킹 오류가 발생한다. 심볼을 찾을 수 없다고 나온다.

클래스 선언과 정의

// A.cpp
class CTest
{
public:
	int m_Val;
};

// Main.cpp
class CTest; // 선언
void main()
{
	CTest t; // Compile Error
}

12 라인에서 컴파일 에러가 난다.

왜일까?

값형은 스택에 할당되고, 스택의 크기는 컴파일 시점에 결정된다.

CTest는 Main.cpp 입장에서는 선언만 돼있다. 어떤 멤버가 있는지, 크기가 몇인지는 알 수 없다.

그렇기 때문에 힙에 할당되는 포인터 형은 가능하다.

class CTest;
void main()
{
	CTest* pT;
}

사실 힙이라기보단 포인터 타입이 OS에 따라 4바이트 혹은 8바이트로 정의되어있기 때문이라고 할 수 있겠다.

해결 방법은?

Main.cpp에도 CTest를 정의해주면 된다. 클래스는 중복 정의를 허용한다.

그래서 보통 헤더 파일에 클래스 정의를 만들고, cpp 파일에서 인클루드 해서 사용한다.

중복 정의를 할 수 있기 때문에 중복된 정의가 다른 경우 문제가 발생할 수 도 있다.

클래스 크기는 같지만, 멤버 함수 순서가 달라 메모리 할당 순서가 바뀌는 경우

 

#define 이 왜?

1. 상수를 정의할 때,

디버깅이 안된다.

왜 디버깅이 안될까?

=> 컴파일 시점에 상수로 바꾼다.

=> symbol table에 들어가지 않는다.

요새는 ide가 해주겠지만, 에러 로그 등에서 확인할 때, 상수명이 아니라 153.2121 같이 숫자 자체가 찍히기 때문에 디버깅하기 어려워 질 수 있다.

#include <iostream>
#define ASPECT_RATIO 1.653

void func(float a);

int main()
{
    if (ASPECT_RATIO > 2) {
        // do someting;
    }

    // 1000줄 코드...

    func(ASPECT_RATIO);

    // 1000줄 코드...

    std::cout << ASPECT_RATIO;
}

이 코드에서 translation unit을 만들면 어떻게 될까?

clang -E fileName.cpp > fileName.pre

이런 식으로 상수로 바뀐다.

symbol table은 무엇일까?

assembler의 symbol table, compiler의 symbol table이 따로 존재한다. 지금은 compiler level의 symbol table을 알아보자.

- 컴파일러에 의해 생성되고 관리되는 "변수명", "함수명", objects, classes, interfaces 등에 대한 자료구조이다.

- 심볼 테이블은 symbol name, type, scope로 구성되고, 드물게 주소가 포함될 때도 있다.

- 컴파일러의 Analysis, Synthesis 단계 모두에서 해당 기호가 정의되었는지, 정확한지 검증하기 위해 사용되고, 이를 통해 중간 코드나 타겟 코드를 만들게 된다.

- 링킹과정에서 링커가 심볼 테이블을 보

- 더 자세한 내용은 아래 링크를 보자.(허수는 안보고 넘어감)

https://www.tutorialspoint.com/compiler_design/compiler_design_symbol_table.htm

https://iq.opengenus.org/symbol-table-in-compiler/

대신에 const 또는 enum을 사용해보자.

const double AspectRatio = 1.653;
class MyClass { private: enum { NumTurns = 5 } };

2. 매크로 함수를 만들때,

매크로 함수 인자로 표현식을 넣었다면, 평가를 여러번하는 오류가 발생할 수 있다.

이런 문제를 해결하기 위해 인라인 함수로 대체해보자.

매크로 처럼 효율적이고, 타입 안정성까지 취할 수 있다.

#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
이런 매크로 함수가 있을 때,

int a = 5;
CALL_WITH_MAX(++a, 0);
CALL_WITH_MAX(++a, 10);

평가가 여러번 될 수 있다.

 

FCM Push 알림

 

FCM은 앱의 키값을 식별하여 특정 스마트폰에 데이터를 전달할 수 있다. 앱의 고유한 키 값은 Firebase에 앱을 등록하면 생성된다. 사용자가 이 앱을 설치하고 알람설정에 동의하면 기기의 FCM token을 얻을 수 있다.

 

 

작업 프로세스

  1. Manifest 수정
    • 서비스 등록
    • 권한 설정
  1. Token 생성
    • 앱이 처음으로 실행되면 자동으로 발급된다.
      • 앱이 새 기기에서 복원 될 때, 사용자가 앱을 제거/재설치할 때, 사용자가 앱 데이터를 지울 때 토큰이 변경될 수 있다.
    • FirebaseMessagingService를 상속받아 onNewToken() 을 오버라이딩 해야한다.
      • 새 토큰이 생성될 때마다 onNewToken() 콜백이 실행된다.
    /**
     * Called if the FCM registration token is updated. This may occur if the security of
     * the previous token had been compromised. Note that this is called when the
     * FCM registration token is initially generated so this is where you would retrieve the token.
     */
    override fun onNewToken(token: String) {
        Log.d(TAG, "Refreshed token: $token")
    
        // If you want to send messages to this application instance or
        // manage this apps subscriptions on the server side, send the
        // FCM registration token to your app server.
        sendRegistrationToServer(token)
    }
  1. 현재 토큰 가져오기
    • 현재 토큰을 검색해야 하는 경우 FirebaseMessaging.getInstance().getToken()을 호출한다.
    FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
        if (!task.isSuccessful) {
            Log.w(TAG, "Fetching FCM registration token failed", task.exception)
            return@OnCompleteListener
        }
    
        // Get new FCM registration token
        val token = task.result
    
        // Log and toast
        val msg = getString(R.string.msg_token_fmt, token)
        Log.d(TAG, msg)
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
    })
  1. 메세지 수신
    • FirebaseMessagingService를 상속받은 서비스 에서 onMessageReceived() 콜백을 오버라이딩 해야한다.
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // TODO(developer): Handle FCM messages here.
        // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
        Log.d(TAG, "From: ${remoteMessage.from}")
    
        // Check if message contains a data payload.
        if (remoteMessage.data.isNotEmpty()) {
            Log.d(TAG, "Message data payload: ${remoteMessage.data}")
    
            if (/* Check if data needs to be processed by long running job */ true) {
                // For long-running tasks (10 seconds or more) use WorkManager.
                scheduleJob()
            } else {
                // Handle message within 10 seconds
                handleNow()
            }
        }
    
        // Check if message contains a notification payload.
        remoteMessage.notification?.let {
            Log.d(TAG, "Message Notification Body: ${it.body}")
        }
    
        // Also if you intend on generating your own notifications as a result of a received FCM
        // message, here is where that should be initiated. See sendNotification method below.
    }
    Notification를 받는 경우, Foreground/Background에 따라 다르게 동작한다.
  1. 안드로이드 알림 생성(Data를 받는 경우)
    private fun sendNotification(title: String, text: String, requestId: Int){
          
            val intent = Intent(this, CardRequestActivity::class.java).apply {
                flags = Intent.FLAG_ACTIVITY_CLEAR_TOP and Intent.FLAG_ACTIVITY_NEW_TASK
            }
            val pendingIntent = PendingIntent.getActivity(this, 0, intent, FLAG_MUTABLE)
    
            val channelId = getString(R.string.notification_channel_id)
            val channelName = getString(R.string.default_notification_channel_name)
            val defaultSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
            val notificationBuilder = NotificationCompat.Builder(this, channelId)
                .setAutoCancel(true)
                .setSound(defaultSound)
                .setContentText(text)
                .setContentTitle(title)
                .setContentIntent(pendingIntent)
                .setSmallIcon(R.drawable.bling)
            // create notification channel
            val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
            notificationManager.createNotificationChannel(channel)
            notificationManager.notify(0, notificationBuilder.build())
        }
    }
    • PendingIntent를 생성해서 연결될 페이지를 지정하면 된다.
     

 

[Android] FCM PUSH Notification (FCM PUSH 알림
https://koharinn.tistory.com/587

Android에서 Firebase 클라우드 메시징 클라이언트 앱 설정
https://firebase.google.com/docs/cloud-messaging/android/client?hl=ko

 

+ Recent posts